code wiki / (root) / nx_imgattr_lib_gate.nx

nx_imgattr_lib_gate.nx source

↩ module page · 260 lines · 15327 B

1// nx_imgattr_lib_gate.nx -- the gate for nx_imgattr_lib. IN-PROCESS: it imports the lib and calls it 2// directly, so nx_gate_bite can mutate the subject and watch teeth die. 3// 4// WHAT IT HAS TO PROVE: 5// - the WRITER can never emit an img whose attributes the data can escape from, however hostile the 6// caption is, and it REFUSES rather than emitting an img with no usable alt; 7// - the READER can say NO. A checker that passes everything passes every test, so every positive 8// tooth is paired with a control on markup that MUST fail; 9// - the reader sees something the incumbent ruler cannot. nx_page_verify's a11y check counts the 10// byte pattern - alt=- across the whole page; that rule is transcribed BY HAND below and asserted 11// to ACQUIT the exact markup our own site serves. So is a present/absent alt test. Only the 12// quote-break axis convicts it. That gap IS the defect, written as a test. 13// 14// THE ORACLES ARE INDEPENDENT ON PURPOSE. A tooth that calls the same function on both sides cannot 15// fail, so nothing here asks ia_scan what the answer should have been: the expected values are 16// literals, and the two foreign grammars (nx_page_verify's alt counter and its src scan) are 17// transcribed here rather than imported. 18// license_tier: ORIGINAL expect_exit: 0 19import "nx_syscalls.nx" 20import "nx_gate_verdict.nx" 21import "nx_imgattr_lib.nx" 22 23const IG_ARENA: i64 = 65536 24const IG_SMALL: i64 = 4096 25 26// The real row live on /compare/koikatsu today. Not a synthetic fixture: a synthetic one would use the 27// vocabulary the author was already thinking in, and this defect was found in shipped data. 28const IG_CAP_REAL: *u8 = "The card's own portrait -- the oracle we match against" as *u8 29const IG_SRC_REAL: *u8 = "/compare/koikatsu/native_kk396262.png" as *u8 30const IG_EXTRA: *u8 = "loading='lazy'" as *u8 31const IG_FILE_REAL: *u8 = "guides/kk_wubsy_mateditor_1.png" as *u8 32const IG_BLANK: *u8 = " " as *u8 33// what a browser actually calls that image today, walked by hand from the served bytes 34const IG_TRUNCATED_ALT: *u8 = "The card" as *u8 35 36// gv_check wants a condition that is EXACTLY 1, so the comparisons are named rather than inlined. 37func ig_eq(a: i64, b: i64) -> i64 { if a == b { return 1 } return 0 } 38func ig_gt(a: i64, b: i64) -> i64 { if a > b { return 1 } return 0 } 39func ig_and(a: i64, b: i64) -> i64 { if a == 1 { if b == 1 { return 1 } } return 0 } 40 41func ig_cat(d: *u8, o: i64, s: *u8) -> i64 { 42 var p: i64 = o 43 var i: i64 = 0 44 while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } 45 d[p] = 0 as u8 46 return p 47} 48 49func ig_streq(a: *u8, b: *u8) -> i64 { 50 var i: i64 = 0 51 while a[i] != (0 as u8) { 52 if a[i] != b[i] { return 0 } 53 i = i + 1 54 } 55 if b[i] != (0 as u8) { return 0 } 56 return 1 57} 58 59func ig_count(h: *u8, need: *u8) -> i64 { 60 let hn: i64 = ia_slen(h) 61 let nn: i64 = ia_slen(need) 62 if nn == 0 { return 0 } 63 var c: i64 = 0 64 var i: i64 = 0 65 while i + nn <= hn { 66 var k: i64 = 0 67 var ok: i64 = 1 68 while k < nn { 69 if h[i+k] != need[k] { ok = 0; k = nn } 70 k = k + 1 71 } 72 if ok == 1 { c = c + 1 } 73 i = i + 1 74 } 75 return c 76} 77 78func ig_has(h: *u8, need: *u8) -> i64 { if ig_count(h, need) > 0 { return 1 } return 0 } 79 80// ---- FOREIGN GRAMMAR 1, TRANSCRIBED BY HAND. nx_page_verify's a11y img/alt rule is two whole-page 81// substring counts: img = the four bytes that open the tag, with-alt = a space followed by alt= . 82// Reproduced here rather than imported, so the disagreement teeth compare two genuinely independent 83// readings of the same bytes. 84func ig_pv_a11y_alt(page: *u8) -> i64 { return ig_count(page, " alt=" as *u8) } 85func ig_pv_a11y_img(page: *u8) -> i64 { return ig_count(page, "<img" as *u8) } 86 87// ---- FOREIGN GRAMMAR 2, TRANSCRIBED BY HAND. nx_page_verify locates a value by finding the byte 88// pattern <name>=' and reading to the NEXT apostrophe. That is also exactly what a browser does with 89// a single-quoted value, which is why it doubles as the oracle for -what does the browser see-. 90func ig_first_val(page: *u8, key: *u8, out: *u8, cap: i64) -> i64 { 91 out[0] = 0 as u8 92 let n: i64 = ia_slen(page) 93 let kl: i64 = ia_slen(key) 94 var i: i64 = 0 95 while i + kl <= n { 96 var k: i64 = 0 97 var ok: i64 = 1 98 while k < kl { 99 if page[i+k] != key[k] { ok = 0; k = kl } 100 k = k + 1 101 } 102 if ok == 1 { 103 var s: i64 = i + kl 104 var o: i64 = 0 105 var run: i64 = 1 106 while run == 1 { 107 if s >= n { run = 0 } 108 if run == 1 { if (page[s] as i64) == IA_SQ { run = 0 } } 109 if run == 1 { if o + 1 >= cap { run = 0 } } 110 if run == 1 { 111 out[o] = page[s] 112 o = o + 1 113 s = s + 1 114 } 115 } 116 out[o] = 0 as u8 117 return o 118 } 119 i = i + 1 120 } 121 return 0 122} 123 124// The markup the OLD emitter produced, assembled the way it produced it: the caption dropped raw into 125// a single-quoted attribute. This is a byte-for-byte reconstruction of what /compare/koikatsu serves. 126func ig_build_unescaped(d: *u8, src: *u8, capt: *u8) -> i64 { 127 var o: i64 = 0 128 o = ig_cat(d, o, "<img src='" as *u8) 129 o = ig_cat(d, o, src) 130 o = ig_cat(d, o, "' alt='" as *u8) 131 o = ig_cat(d, o, capt) 132 o = ig_cat(d, o, "' " as *u8) 133 o = ig_cat(d, o, IG_EXTRA) 134 o = ig_cat(d, o, ">" as *u8) 135 return o 136} 137 138// THE DETECTOR UNDER TEST, as one predicate so it can be bitten in both directions: 139// 1 = this markup holds an img a browser cannot read as intended. The three reasons are asserted 140// SEPARATELY by the teeth below, so the reason travels with the verdict instead of collapsing to a bit. 141func ig_img_unreadable(markup: *u8) -> i64 { 142 let sl: *i64 = sys_mmap(IA_SCAN_SLOTS * IA_SLOT_BYTES) as *i64 143 ia_scan(markup, ia_slen(markup), sl) 144 if sl[IA_S_IMGS] == 0 { return 0 } 145 if sl[IA_S_ALT_OK] < sl[IA_S_IMGS] { return 1 } 146 if sl[IA_S_QUOTEBREAK] > 0 { return 1 } 147 if sl[IA_S_MALFORMED] > 0 { return 1 } 148 return 0 149} 150 151func main() -> i64 { 152 let ctr: *i64 = gv_ctr() 153 gv_head("nx_imgattr_lib_gate -- an img tag our own site can be read by" as *u8) 154 155 let bad: *u8 = sys_mmap(IG_ARENA) 156 let good: *u8 = sys_mmap(IG_ARENA) 157 let alt: *u8 = sys_mmap(IG_SMALL) 158 let got: *u8 = sys_mmap(IG_SMALL) 159 let sl: *i64 = sys_mmap(IA_SCAN_SLOTS * IA_SLOT_BYTES) as *i64 160 161 // ---- the fixture must be able to fail before anything is asserted about the outcome 162 gv_check("fixture-reached-the-condition-the-live-caption-carries-a-raw-apostrophe" as *u8, ia_attr_hostile(IG_CAP_REAL), ctr) 163 ig_build_unescaped(bad, IG_SRC_REAL, IG_CAP_REAL) 164 gv_check("fixture-reached-the-condition-the-reconstructed-served-markup-is-one-img-tag" as *u8, ig_eq(ig_pv_a11y_img(bad), 1), ctr) 165 166 // ---- WHAT A BROWSER ACTUALLY CALLS THAT IMAGE, by the single-quote rule, against a literal 167 ig_first_val(bad, "alt='" as *u8, got, IG_SMALL) 168 gv_check("served-markup-the-browser-visible-alt-is-only-the-two-words-The-card" as *u8, ig_streq(got, IG_TRUNCATED_ALT), ctr) 169 170 // ---- BOTH INCUMBENT TESTS ACQUIT IT. This is why the quote-break axis had to exist. 171 gv_check("neg-control-the-substring-counter-nx_page_verify-uses-calls-it-alt-PRESENT" as *u8, ig_eq(ig_pv_a11y_alt(bad), 1), ctr) 172 ia_scan(bad, ia_slen(bad), sl) 173 gv_check("neg-control-a-present-absent-alt-test-ALSO-acquits-it-the-value-is-non-empty" as *u8, ig_eq(sl[IA_S_ALT_OK], sl[IA_S_IMGS]), ctr) 174 gv_check("neg-control-the-tag-still-closes-so-the-malformed-axis-acquits-it-too" as *u8, ig_eq(sl[IA_S_MALFORMED], 0), ctr) 175 gv_check("quote-break-axis-CONVICTS-the-served-markup-where-every-other-axis-acquits" as *u8, ig_eq(sl[IA_S_QUOTEBREAK], 1), ctr) 176 177 // ---- the emitter, on that same hostile data 178 let altn: i64 = ia_alt_derive(IG_CAP_REAL, IG_SRC_REAL, alt, ia_alt_cap_for(IG_CAP_REAL, IG_SRC_REAL)) 179 gv_check("alt-derivation-returns-the-caption-whole-when-the-row-carries-one" as *u8, ig_streq(alt, IG_CAP_REAL), ctr) 180 gv_check("alt-derivation-reports-the-length-it-wrote" as *u8, ig_eq(altn, ia_slen(IG_CAP_REAL)), ctr) 181 let cap2: i64 = ia_img_cap_for(IG_SRC_REAL, alt, IG_EXTRA) 182 let wrote: i64 = ia_img_emit(good, 0, cap2, IG_SRC_REAL, alt, IG_EXTRA) 183 gv_check("emitter-writes-a-tag-rather-than-refusing-on-valid-data" as *u8, ig_gt(wrote, 0), ctr) 184 gv_check("sizing-is-derived-the-computed-cap-held-the-whole-worst-case-emit" as *u8, ig_eq(wrote, ia_slen(good)), ctr) 185 186 ia_scan(good, ia_slen(good), sl) 187 gv_subjects("img-elements-the-fixed-emitter-produced" as *u8, sl[IA_S_IMGS], ctr) 188 gv_check("every-emitted-img-carries-a-non-empty-alt" as *u8, ig_and(ig_gt(sl[IA_S_IMGS], 0), ig_eq(sl[IA_S_ALT_OK], sl[IA_S_IMGS])), ctr) 189 gv_check("every-emitted-img-carries-a-non-empty-src" as *u8, ig_and(ig_gt(sl[IA_S_IMGS], 0), ig_eq(sl[IA_S_SRC_OK], sl[IA_S_IMGS])), ctr) 190 gv_check("no-emitted-tag-lets-its-own-data-escape-the-attribute" as *u8, ig_eq(sl[IA_S_QUOTEBREAK], 0), ctr) 191 gv_check("no-emitted-tag-is-left-unclosed" as *u8, ig_eq(sl[IA_S_MALFORMED], 0), ctr) 192 193 // ---- the src must survive the OTHER organ's grammar, WHOLE, not merely be found 194 ig_first_val(good, "src='" as *u8, got, IG_SMALL) 195 gv_check("src-is-recovered-WHOLE-by-the-page_verify-grammar" as *u8, ig_streq(got, IG_SRC_REAL), ctr) 196 197 // ---- rule 25: the byte is KEPT, where the incumbent escaper replaces it with a space 198 ig_first_val(good, "alt='" as *u8, got, IG_SMALL) 199 gv_check("escaped-alt-survives-the-single-quote-rule-to-its-last-word" as *u8, ig_has(got, "match against" as *u8), ctr) 200 gv_check("escaper-keeps-the-apostrophe-as-a-numeric-reference-rather-than-dropping-it" as *u8, ig_has(good, "card&" as *u8), ctr) 201 gv_check("neg-control-the-apostrophe-was-not-replaced-by-a-space-the-lossy-incumbent-way" as *u8, ig_eq(ig_has(good, "card s own" as *u8), 0), ctr) 202 203 gv_bite("neg-control-detector-fires-on-the-served-markup-and-is-silent-on-the-fixed-one" as *u8, ig_img_unreadable(bad), ig_img_unreadable(good), ctr) 204 205 // ---- the reader must be able to say NO for the ordinary reasons too 206 gv_check("neg-control-img-with-no-alt-attribute-at-all-reads-unusable" as *u8, ig_eq(ig_img_unreadable("<img src='/a.png'>" as *u8), 1), ctr) 207 gv_check("neg-control-img-with-an-empty-alt-reads-unusable" as *u8, ig_eq(ig_img_unreadable("<img src='/a.png' alt=''>" as *u8), 1), ctr) 208 gv_check("neg-control-img-with-a-whitespace-only-alt-reads-unusable" as *u8, ig_eq(ig_img_unreadable("<img src='/a.png' alt=' '>" as *u8), 1), ctr) 209 gv_check("positive-control-a-plain-well-formed-img-reads-usable" as *u8, ig_eq(ig_img_unreadable("<img src='/a.png' alt='a red square'>" as *u8), 0), ctr) 210 gv_check("attribute-order-does-not-decide-the-answer-alt-before-src-still-reads" as *u8, ig_eq(ig_img_unreadable("<img alt='a red square' src='/a.png'>" as *u8), 0), ctr) 211 gv_check("double-quoted-and-unquoted-attribute-forms-both-read" as *u8, ig_eq(ig_img_unreadable("<img src=/a.png alt=square>" as *u8), 0), ctr) 212 213 ia_scan("<img src='/a.png' alt='never closed" as *u8, ia_slen("<img src='/a.png' alt='never closed" as *u8), sl) 214 gv_check("unterminated-attribute-value-is-reported-MALFORMED-not-clean" as *u8, ig_eq(sl[IA_S_MALFORMED], 1), ctr) 215 216 // ---- the emitter REFUSES rather than shipping an unusable img 217 let blankalt: i64 = ia_alt_derive(IG_BLANK, "x/.png" as *u8, alt, IG_SMALL) 218 gv_check("alt-derivation-returns-nothing-when-neither-caption-nor-file-name-can-supply-one" as *u8, ig_eq(blankalt, 0), ctr) 219 let refused: i64 = ia_img_emit(good, 0, IG_ARENA, IG_SRC_REAL, alt, IG_EXTRA) 220 gv_check("emitter-REFUSES-and-writes-nothing-when-no-alt-can-be-derived" as *u8, ig_and(ig_eq(refused, 0), ig_eq(ia_slen(good), 0)), ctr) 221 let nosrc: i64 = ia_img_emit(good, 0, IG_ARENA, IG_BLANK, IG_CAP_REAL, IG_EXTRA) 222 gv_check("emitter-REFUSES-and-writes-nothing-when-the-src-is-blank" as *u8, ig_and(ig_eq(nosrc, 0), ig_eq(ia_slen(good), 0)), ctr) 223 224 // ---- the alt is DATA when the row has no caption, never a generic word 225 let dlen: i64 = ia_alt_derive(IG_BLANK, IG_FILE_REAL, alt, ia_alt_cap_for(IG_BLANK, IG_FILE_REAL)) 226 gv_check("alt-is-DERIVED-from-the-file-name-when-the-row-carries-no-caption" as *u8, ig_streq(alt, "kk wubsy mateditor 1" as *u8), ctr) 227 gv_check("neg-control-the-derived-alt-is-never-the-generic-word-image" as *u8, ig_eq(ig_streq(alt, "image" as *u8), 0), ctr) 228 gv_check("derived-alt-is-non-empty-so-the-emitter-can-proceed" as *u8, ig_gt(dlen, 0), ctr) 229 230 // ---- the reader must not invent subjects 231 gv_check("neg-control-an-img-quoted-inside-a-script-body-is-not-an-element" as *u8, ig_eq(ia_scan("<script>var s='<img src=x alt=y>'</script>" as *u8, ia_slen("<script>var s='<img src=x alt=y>'</script>" as *u8), sl), 0), ctr) 232 gv_check("neg-control-an-img-inside-a-comment-is-not-an-element" as *u8, ig_eq(ia_scan("<!-- <img src=a alt=b> -->" as *u8, ia_slen("<!-- <img src=a alt=b> -->" as *u8), sl), 0), ctr) 233 gv_check("neg-control-the-word-images-is-not-counted-as-an-img-element" as *u8, ig_eq(ia_scan("<images>" as *u8, ia_slen("<images>" as *u8), sl), 0), ctr) 234 gv_check("positive-control-a-real-img-beside-all-three-of-those-IS-counted" as *u8, ig_eq(ia_scan("<!-- <img src=a> --><script>'<img src=b>'</script><images><img src='/c.png' alt='c'>" as *u8, ia_slen("<!-- <img src=a> --><script>'<img src=b>'</script><images><img src='/c.png' alt='c'>" as *u8), sl), 1), ctr) 235 236 // ---- the OTHER quote character is data too. Built byte-wise: nx_cc has no escape for a double 237 // quote inside a string literal, and building it proves the byte rather than trusting one. 238 let dqcap: *u8 = sys_mmap(IG_SMALL) 239 var q: i64 = 0 240 q = ig_cat(dqcap, q, "a " as *u8) 241 dqcap[q] = IA_DQ as u8 242 q = q + 1 243 q = ig_cat(dqcap, q, "quoted" as *u8) 244 dqcap[q] = IA_DQ as u8 245 q = q + 1 246 dqcap[q] = 0 as u8 247 gv_check("fixture-reached-the-condition-the-double-quote-caption-really-holds-that-byte" as *u8, ia_attr_hostile(dqcap), ctr) 248 let dqbuf: *u8 = sys_mmap(IG_ARENA) 249 ia_img_emit(dqbuf, 0, ia_img_cap_for(IG_SRC_REAL, dqcap, IG_EXTRA), IG_SRC_REAL, dqcap, IG_EXTRA) 250 gv_check("a-double-quote-in-the-data-also-cannot-escape-its-attribute" as *u8, ig_eq(ig_img_unreadable(dqbuf), 0), ctr) 251 gv_check("the-double-quote-byte-is-KEPT-as-an-entity-not-dropped" as *u8, ig_has(dqbuf, "&quot;" as *u8), ctr) 252 253 // ---- an ampersand already in the data is escaped once, and not again 254 let ampbuf: *u8 = sys_mmap(IG_ARENA) 255 ia_img_emit(ampbuf, 0, ia_img_cap_for(IG_SRC_REAL, "black &amp; white" as *u8, IG_EXTRA), IG_SRC_REAL, "black &amp; white" as *u8, IG_EXTRA) 256 gv_check("an-ampersand-already-in-the-data-is-escaped-once" as *u8, ig_eq(ig_count(ampbuf, "&amp;amp;" as *u8), 1), ctr) 257 gv_check("neg-control-that-same-emit-is-not-escaped-a-second-time" as *u8, ig_eq(ig_count(ampbuf, "&amp;amp;amp;" as *u8), 0), ctr) 258 259 return gv_verdict("nx_imgattr_lib" as *u8, ctr, "one definition of an img with usable attributes, shared by the emitter that writes it and the reader that checks it" as *u8) 260}