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, """ 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 & white" as *u8, IG_EXTRA), IG_SRC_REAL, "black & 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;" 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;" 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}