nx_hpack.nx source
↩ module page · 541 lines · 26297 B
1// nx_hpack.nx -- TUTOR-BOOTSTRAP SCAFFOLD (Claude, authored under the R4-H2
2// HTTP/2-transport-ladder workflow), NOT credited as team self-authoring.
3//
4// R4-H2-002 of the sovereign HTTP/2 transport ladder
5// (knowledge/specs/2026-06-13-http2-transport-ladder.md). A minimal sovereign
6// HPACK (RFC 7541) header-block coder: the WIRE FORMAT (N-bit-prefix integers,
7// length-prefixed string literals, the static table, the 6.x field
8// representations) is an allowed internet-boundary requirement -- the bytes are
9// dictated by RFC 7541 byte-for-byte so we interoperate, exactly as
10// nx_tls13_ext.nx emits RFC 8446 extension bytes. The IMPLEMENTATION is pure
11// NishiLang via nx_cc -> nxasm_x86: no gcc, no nghttp2, no third-party HPACK.
12//
13// FOUNDED ON (composes, does not reinvent -- anti-orphan law): nx_str.nx (which
14// transitively splices nx_syscalls.nx via syscalls.nx -- so we import nx_str.nx
15// ONCE and inherit sys_mmap/sys_write/sys_exit; importing nx_syscalls.nx
16// directly TOO would be the RC6 double-import landmine). No floating
17// capability: HPACK is the lowest rung; the frame codec founds on it next.
18//
19// ENCODER (client request side, Huffman NOT required -- servers accept raw
20// literals): indexed-header-field (6.1), literal-with-incremental-indexing
21// indexed-name + new-name (6.2.1), literal-without-indexing (6.2.2), literal
22// never-indexed (6.2.3), plus the N-bit-prefix integer (5.1) and the raw
23// (H=0) string literal (5.2). DECODER (lenient response side): N-bit-prefix
24// integer decode with truncation rejection, string-literal decode with length
25// rejection, and a static-table reverse lookup enough to read :status / walk a
26// header block.
27//
28// BACK-FILL: the team RE-AUTHORS this from the DATA spec via the
29// emitter-of-emitters (X-AUT-006c/e/f) -- this hand-authored scaffold is the
30// sanctioned one-time bootstrap only (meter-integrity, mirror
31// nx_frame_codec.nx:13-16).
32//
33// GATE (main): asserts byte-exact the RFC 7541 known-answers -- §5.1.1 INT
34// N=5 value 10 (0A), N=5 value 1337 (1F 9A 0A), N=8 value 42 (2A); Appendix
35// C.2.1 lit-inc new-name "custom-key: custom-header"; C.2.2 lit-noindex
36// ":path: /sample/path"; C.2.3 lit-never "password: secret"; C.2.4 indexed
37// ":method: GET" (82); C.3.1 first request (82 86 84 41 0f www.example.com);
38// decode round-trips; static-table lookups -- PLUS two TAMPER cases (a
39// truncated integer continuation and an over-long string length) that MUST be
40// rejected with a negative return.
41//
42// license_tier: INDEPENDENT_REDERIVE
43// genealogy_id: international-research-sources/ietf/rfc_7541
44// lineage_id: nishi_hpack_r4h2_002
45
46import "nx_str.nx"
47import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
48const K_MAGIC_1337: i64 = 1337
49
50// ---- print helpers (the fc_puts/fc_putn pattern, renamed per organ) ----
51func hp_puts(s: *u8) -> i64 {
52 var n: i64 = 0
53 while s[n] != (0 as u8) { n = n + 1 }
54 sys_write(1, s, n)
55 return 0
56}
57// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
58// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
59// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
60// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
61func hp_putn(v: i64) -> i64 { nxi_out(v); return 0 }
62// fd-aware decimal writer (the scfn pattern from nx_sreach_scorecard.nx:30) --
63// used ONLY by the durable status-line append at the end of the gate.
64// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
65// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
66// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
67// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
68func hp_fdn(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 }
69func hp_puthex2(v: i64) -> i64 {
70 let h: *u8 = sys_mmap(4)
71 let d0: i64 = (v >> 4) & 0xf
72 let d1: i64 = v & 0xf
73 if d0 < 10 { h[0] = (48 + d0) as u8 } else { h[0] = (87 + d0) as u8 }
74 if d1 < 10 { h[1] = (48 + d1) as u8 } else { h[1] = (87 + d1) as u8 }
75 sys_write(1, h, 2)
76 return 0
77}
78
79// =====================================================================
80// 5.1 N-bit-prefix integer representation (RFC 7541 §5.1)
81// =====================================================================
82//
83// prefix_bits = N (1..8). `flags` carries the high (8-N) bits of the FIRST
84// octet (the representation's leading bit-pattern, already shifted into place
85// by the caller, e.g. 0x80 for an indexed field). Returns the new offset.
86//
87// If value < 2^N - 1: store value in the low N bits.
88// Else: store 2^N - 1 (all-ones prefix), value -= (2^N - 1); while value >= 128
89// emit (value % 128) + 128 and value /= 128; emit the final value (< 128).
90func hpack_encode_int(out: *u8, off: i64, prefix_bits: i64, flags: i64, value: i64) -> i64 {
91 if prefix_bits < 1 { return 0 - 1 }
92 if prefix_bits > 8 { return 0 - 1 }
93 if value < 0 { return 0 - 1 }
94 var max_prefix: i64 = 1
95 var b: i64 = 0
96 while b < prefix_bits { max_prefix = max_prefix * 2; b = b + 1 }
97 max_prefix = max_prefix - 1 // 2^N - 1
98 var o: i64 = off
99 if value < max_prefix {
100 out[o] = ((flags & 0xff) | value) as u8
101 o = o + 1
102 return o
103 }
104 out[o] = ((flags & 0xff) | max_prefix) as u8
105 o = o + 1
106 var rem: i64 = value - max_prefix
107 while rem >= 128 {
108 out[o] = ((rem % 128) + 128) as u8
109 o = o + 1
110 rem = rem / 128
111 }
112 out[o] = rem as u8
113 o = o + 1
114 return o
115}
116
117// Decode an N-bit-prefix integer. `off` points at the first octet; `lim` is
118// one-past-the-last readable octet. Writes the value to *out_val, returns the
119// new offset, or < 0 on error: -1 = truncated (continuation runs past lim),
120// -2 = bad prefix_bits. This is a TAMPER-rejecting decode: a prefix of
121// all-ones with no following octet inside lim returns -1, never a partial value.
122func hpack_decode_int(buf: *u8, off: i64, lim: i64, prefix_bits: i64, out_val: *i64) -> i64 {
123 if prefix_bits < 1 { return 0 - 2 }
124 if prefix_bits > 8 { return 0 - 2 }
125 if off >= lim { return 0 - 1 }
126 var max_prefix: i64 = 1
127 var b: i64 = 0
128 while b < prefix_bits { max_prefix = max_prefix * 2; b = b + 1 }
129 max_prefix = max_prefix - 1
130 var o: i64 = off
131 let first: i64 = (buf[o] & 0xff) & max_prefix
132 o = o + 1
133 if first < max_prefix {
134 *out_val = first
135 return o
136 }
137 var val: i64 = max_prefix
138 var m: i64 = 0
139 var more: i64 = 1
140 while more == 1 {
141 if o >= lim { return 0 - 1 } // truncated continuation -> reject
142 let oct: i64 = buf[o] & 0xff
143 o = o + 1
144 val = val + ((oct & 127) << m)
145 m = m + 7
146 if (oct & 128) == 0 { more = 0 }
147 if m > 63 { return 0 - 1 } // overflow guard (malicious run)
148 }
149 *out_val = val
150 return o
151}
152
153// =====================================================================
154// 5.2 String literal (RFC 7541 §5.2), H=0 raw only on the wire we EMIT.
155// =====================================================================
156//
157// | H | Length(7+) | followed by Length octets. We always emit H=0 (raw).
158func hpack_encode_str_raw(out: *u8, off: i64, s: *u8, slen: i64) -> i64 {
159 if slen < 0 { return 0 - 1 }
160 // length prefix: N=7 integer, flags=0 (H bit = 0 in the top bit).
161 var o: i64 = hpack_encode_int(out, off, 7, 0, slen)
162 if o < 0 { return o }
163 var i: i64 = 0
164 while i < slen { out[o + i] = s[i]; i = i + 1 }
165 return o + slen
166}
167
168// Decode a string literal. Writes the offset of the string bytes (relative to
169// buf, as an absolute index) to *out_ptr, the length to *out_len, and the
170// Huffman flag (1/0) to *out_huff. Returns the new offset, or < 0:
171// -1 = truncated length integer, -3 = declared length exceeds lim (TAMPER).
172// v1 does NOT Huffman-decode; it reports out_huff so the caller can skip/flag.
173func hpack_decode_str(buf: *u8, off: i64, lim: i64, out_ptr: *i64, out_len: *i64, out_huff: *i64) -> i64 {
174 if off >= lim { return 0 - 1 }
175 let huff: i64 = (buf[off] >> 7) & 1
176 let lenbox: *i64 = sys_mmap(16) as *i64
177 let after: i64 = hpack_decode_int(buf, off, lim, 7, lenbox)
178 if after < 0 { return after } // truncated length integer
179 let slen: i64 = lenbox[0]
180 if slen < 0 { return 0 - 3 }
181 if after + slen > lim { return 0 - 3 } // length runs past buffer -> reject
182 *out_ptr = after
183 *out_len = slen
184 *out_huff = huff
185 return after + slen
186}
187
188// =====================================================================
189// 6.1 Indexed Header Field -- first byte = 0x80 | index, N=7 prefix.
190// =====================================================================
191func hpack_encode_indexed(out: *u8, off: i64, index: i64) -> i64 {
192 if index < 1 { return 0 - 1 }
193 return hpack_encode_int(out, off, 7, 0x80, index)
194}
195
196// =====================================================================
197// 6.2.1 Literal w/ Incremental Indexing
198// =====================================================================
199// indexed-name: lead pattern 01, N=6 prefix -> flags 0x40.
200func hpack_encode_lit_inc_indexed_name(out: *u8, off: i64, name_index: i64, val: *u8, vlen: i64) -> i64 {
201 if name_index < 1 { return 0 - 1 }
202 var o: i64 = hpack_encode_int(out, off, 6, 0x40, name_index)
203 if o < 0 { return o }
204 return hpack_encode_str_raw(out, o, val, vlen)
205}
206// new-name: byte 0x40 (name index 0 in the N=6 prefix), then name string, then value string.
207func hpack_encode_lit_inc_new_name(out: *u8, off: i64, name: *u8, nlen: i64, val: *u8, vlen: i64) -> i64 {
208 var o: i64 = hpack_encode_int(out, off, 6, 0x40, 0)
209 if o < 0 { return o }
210 o = hpack_encode_str_raw(out, o, name, nlen)
211 if o < 0 { return o }
212 return hpack_encode_str_raw(out, o, val, vlen)
213}
214
215// =====================================================================
216// 6.2.2 Literal w/o Indexing -- indexed name, lead 0000, N=4 prefix (flags 0x00).
217// =====================================================================
218func hpack_encode_lit_noindex_indexed_name(out: *u8, off: i64, name_index: i64, val: *u8, vlen: i64) -> i64 {
219 if name_index < 1 { return 0 - 1 }
220 var o: i64 = hpack_encode_int(out, off, 4, 0x00, name_index)
221 if o < 0 { return o }
222 return hpack_encode_str_raw(out, o, val, vlen)
223}
224
225// =====================================================================
226// 6.2.3 Literal Never Indexed -- new name, byte 0x10, then name, then value.
227// =====================================================================
228func hpack_encode_lit_never_new_name(out: *u8, off: i64, name: *u8, nlen: i64, val: *u8, vlen: i64) -> i64 {
229 var o: i64 = hpack_encode_int(out, off, 4, 0x10, 0)
230 if o < 0 { return o }
231 o = hpack_encode_str_raw(out, o, name, nlen)
232 if o < 0 { return o }
233 return hpack_encode_str_raw(out, o, val, vlen)
234}
235
236// =====================================================================
237// Static table (Appendix A) reverse lookup -- name+value -> index, or 0.
238// =====================================================================
239// Only the anchors this client needs (the spec's named subset); 0 = not found.
240// Byte-literal compares against the static (name,value) pairs.
241func hp_eq_bytes(a: *u8, alen: i64, lit: *u8) -> i64 {
242 var litlen: i64 = 0
243 while lit[litlen] != (0 as u8) { litlen = litlen + 1 }
244 if alen != litlen { return 0 }
245 var i: i64 = 0
246 while i < alen { if (a[i] & 0xff) != (lit[i] & 0xff) { return 0 } i = i + 1 }
247 return 1
248}
249func hpack_static_index(name: *u8, nlen: i64, val: *u8, vlen: i64) -> i64 {
250 // :authority (name-only, value empty) = 1
251 if hp_eq_bytes(name, nlen, ":authority" as *u8) == 1 {
252 if vlen == 0 { return 1 }
253 return 0
254 }
255 if hp_eq_bytes(name, nlen, ":method" as *u8) == 1 {
256 if hp_eq_bytes(val, vlen, "GET" as *u8) == 1 { return 2 }
257 if hp_eq_bytes(val, vlen, "POST" as *u8) == 1 { return 3 }
258 return 0
259 }
260 if hp_eq_bytes(name, nlen, ":path" as *u8) == 1 {
261 if hp_eq_bytes(val, vlen, "/" as *u8) == 1 { return 4 }
262 if hp_eq_bytes(val, vlen, "/index.html" as *u8) == 1 { return 5 }
263 return 0
264 }
265 if hp_eq_bytes(name, nlen, ":scheme" as *u8) == 1 {
266 if hp_eq_bytes(val, vlen, "http" as *u8) == 1 { return 6 }
267 if hp_eq_bytes(val, vlen, "https" as *u8) == 1 { return 7 }
268 return 0
269 }
270 if hp_eq_bytes(name, nlen, ":status" as *u8) == 1 {
271 if hp_eq_bytes(val, vlen, "200" as *u8) == 1 { return 8 }
272 return 0
273 }
274 return 0
275}
276
277// =====================================================================
278// Decoder: indexed-field detection + static :status lookup, header walk.
279// =====================================================================
280// Read one header field starting at off; classify it. Returns new off or < 0.
281// For an indexed field referencing :status 200 (idx 8) writes 200 to *out_status
282// (else leaves it). Lenient: literal/never-indexed fields are walked (skipped)
283// to find the block end; Huffman string bytes are stepped over via out_huff.
284func hpack_decode_walk_field(buf: *u8, off: i64, lim: i64, out_status: *i64) -> i64 {
285 if off >= lim { return 0 - 1 }
286 let lead: i64 = buf[off] & 0xff
287 if (lead & 0x80) == 0x80 {
288 // 6.1 indexed
289 let ibox: *i64 = sys_mmap(16) as *i64
290 let after: i64 = hpack_decode_int(buf, off, lim, 7, ibox)
291 if after < 0 { return after }
292 if ibox[0] == 8 { *out_status = 200 } // static :status 200
293 return after
294 }
295 if (lead & 0x40) == 0x40 {
296 // 6.2.1 literal w/ incremental indexing, N=6 name index
297 let nbox: *i64 = sys_mmap(16) as *i64
298 var o: i64 = hpack_decode_int(buf, off, lim, 6, nbox)
299 if o < 0 { return o }
300 if nbox[0] == 0 {
301 // new name: skip name string
302 let np: *i64 = sys_mmap(16) as *i64
303 let nl: *i64 = sys_mmap(16) as *i64
304 let nh: *i64 = sys_mmap(16) as *i64
305 o = hpack_decode_str(buf, o, lim, np, nl, nh)
306 if o < 0 { return o }
307 }
308 // value string
309 let vp: *i64 = sys_mmap(16) as *i64
310 let vl: *i64 = sys_mmap(16) as *i64
311 let vh: *i64 = sys_mmap(16) as *i64
312 o = hpack_decode_str(buf, o, lim, vp, vl, vh)
313 return o
314 }
315 // 6.2.2 (0000 xxxx) or 6.2.3 (0001 xxxx): N=4 name index, then value.
316 let nbox2: *i64 = sys_mmap(16) as *i64
317 var o2: i64 = hpack_decode_int(buf, off, lim, 4, nbox2)
318 if o2 < 0 { return o2 }
319 if nbox2[0] == 0 {
320 let np2: *i64 = sys_mmap(16) as *i64
321 let nl2: *i64 = sys_mmap(16) as *i64
322 let nh2: *i64 = sys_mmap(16) as *i64
323 o2 = hpack_decode_str(buf, o2, lim, np2, nl2, nh2)
324 if o2 < 0 { return o2 }
325 }
326 let vp2: *i64 = sys_mmap(16) as *i64
327 let vl2: *i64 = sys_mmap(16) as *i64
328 let vh2: *i64 = sys_mmap(16) as *i64
329 o2 = hpack_decode_str(buf, o2, lim, vp2, vl2, vh2)
330 return o2
331}
332
333// =====================================================================
334// GATE (main): RFC 7541 byte-exact KATs + decode round-trips + 2 tamper cases
335// =====================================================================
336func hp_check_bytes(name: *u8, got: *u8, glen: i64, exp: *u8, elen: i64) -> i64 {
337 if glen != elen {
338 hp_puts(" FAIL " as *u8); hp_puts(name)
339 hp_puts(" length got=" as *u8); hp_putn(glen)
340 hp_puts(" exp=" as *u8); hp_putn(elen); hp_puts("\n" as *u8)
341 return 0
342 }
343 var i: i64 = 0
344 while i < glen {
345 if (got[i] & 0xff) != (exp[i] & 0xff) {
346 hp_puts(" FAIL " as *u8); hp_puts(name)
347 hp_puts(" byte[" as *u8); hp_putn(i)
348 hp_puts("] got=" as *u8); hp_puthex2(got[i] & 0xff)
349 hp_puts(" exp=" as *u8); hp_puthex2(exp[i] & 0xff); hp_puts("\n" as *u8)
350 return 0
351 }
352 i = i + 1
353 }
354 hp_puts(" PASS " as *u8); hp_puts(name)
355 hp_puts(" (" as *u8); hp_putn(glen); hp_puts(" bytes)\n" as *u8)
356 return 1
357}
358
359func main() -> i64 {
360 var pass: i64 = 0
361 var tot: i64 = 0
362 hp_puts("nx_hpack gate (RFC 7541 HPACK, FOUNDED on nx_str/nx_syscalls)\n" as *u8)
363
364 let out: *u8 = sys_mmap(256)
365 let exp: *u8 = sys_mmap(256)
366
367 // ---- §5.1.1 INT N=5 value 10 -> 0A (flags 0) ----
368 var o: i64 = hpack_encode_int(out, 0, 5, 0, 10)
369 exp[0] = 0x0a as u8
370 pass = pass + hp_check_bytes("INT N=5 v=10" as *u8, out, o, exp, 1); tot = tot + 1
371
372 // ---- §5.1.1 INT N=5 value 1337 -> 1F 9A 0A ----
373 o = hpack_encode_int(out, 0, 5, 0, K_MAGIC_1337)
374 exp[0] = 0x1f as u8; exp[1] = 0x9a as u8; exp[2] = 0x0a as u8
375 pass = pass + hp_check_bytes("INT N=5 v=1337" as *u8, out, o, exp, 3); tot = tot + 1
376
377 // ---- §5.1.1 INT N=8 value 42 -> 2A ----
378 o = hpack_encode_int(out, 0, 8, 0, 42)
379 exp[0] = 0x2a as u8
380 pass = pass + hp_check_bytes("INT N=8 v=42" as *u8, out, o, exp, 1); tot = tot + 1
381
382 // ---- C.2.4 indexed :method GET -> 82 ----
383 o = hpack_encode_indexed(out, 0, 2)
384 exp[0] = 0x82 as u8
385 pass = pass + hp_check_bytes("C.2.4 indexed :method GET" as *u8, out, o, exp, 1); tot = tot + 1
386
387 // ---- C.2.1 lit-inc new-name "custom-key: custom-header" ----
388 let ckey: *u8 = sys_mmap(32); nx_str_cpy(ckey, "custom-key" as *u8)
389 let chdr: *u8 = sys_mmap(32); nx_str_cpy(chdr, "custom-header" as *u8)
390 o = hpack_encode_lit_inc_new_name(out, 0, ckey, 10, chdr, 13)
391 // 40 0a 63 75 73 74 6f 6d 2d 6b 65 79 0d 63 75 73 74 6f 6d 2d 68 65 61 64 65 72
392 exp[0]=0x40 as u8; exp[1]=0x0a as u8
393 exp[2]=0x63 as u8; exp[3]=0x75 as u8; exp[4]=0x73 as u8; exp[5]=0x74 as u8
394 exp[6]=0x6f as u8; exp[7]=0x6d as u8; exp[8]=0x2d as u8; exp[9]=0x6b as u8
395 exp[10]=0x65 as u8; exp[11]=0x79 as u8
396 exp[12]=0x0d as u8
397 exp[13]=0x63 as u8; exp[14]=0x75 as u8; exp[15]=0x73 as u8; exp[16]=0x74 as u8
398 exp[17]=0x6f as u8; exp[18]=0x6d as u8; exp[19]=0x2d as u8; exp[20]=0x68 as u8
399 exp[21]=0x65 as u8; exp[22]=0x61 as u8; exp[23]=0x64 as u8; exp[24]=0x65 as u8
400 exp[25]=0x72 as u8
401 pass = pass + hp_check_bytes("C.2.1 lit-inc new-name" as *u8, out, o, exp, 26); tot = tot + 1
402
403 // ---- C.2.2 lit-noindex ":path: /sample/path", name index 4 ----
404 let spath: *u8 = sys_mmap(32); nx_str_cpy(spath, "/sample/path" as *u8)
405 o = hpack_encode_lit_noindex_indexed_name(out, 0, 4, spath, 12)
406 // 04 0c 2f 73 61 6d 70 6c 65 2f 70 61 74 68
407 exp[0]=0x04 as u8; exp[1]=0x0c as u8
408 exp[2]=0x2f as u8; exp[3]=0x73 as u8; exp[4]=0x61 as u8; exp[5]=0x6d as u8
409 exp[6]=0x70 as u8; exp[7]=0x6c as u8; exp[8]=0x65 as u8; exp[9]=0x2f as u8
410 exp[10]=0x70 as u8; exp[11]=0x61 as u8; exp[12]=0x74 as u8; exp[13]=0x68 as u8
411 pass = pass + hp_check_bytes("C.2.2 lit-noindex :path" as *u8, out, o, exp, 14); tot = tot + 1
412
413 // ---- C.2.3 lit-never new-name "password: secret" ----
414 let pkey: *u8 = sys_mmap(32); nx_str_cpy(pkey, "password" as *u8)
415 let pval: *u8 = sys_mmap(32); nx_str_cpy(pval, "secret" as *u8)
416 o = hpack_encode_lit_never_new_name(out, 0, pkey, 8, pval, 6)
417 // 10 08 70 61 73 73 77 6f 72 64 06 73 65 63 72 65 74
418 exp[0]=0x10 as u8; exp[1]=0x08 as u8
419 exp[2]=0x70 as u8; exp[3]=0x61 as u8; exp[4]=0x73 as u8; exp[5]=0x73 as u8
420 exp[6]=0x77 as u8; exp[7]=0x6f as u8; exp[8]=0x72 as u8; exp[9]=0x64 as u8
421 exp[10]=0x06 as u8
422 exp[11]=0x73 as u8; exp[12]=0x65 as u8; exp[13]=0x63 as u8; exp[14]=0x72 as u8
423 exp[15]=0x65 as u8; exp[16]=0x74 as u8
424 pass = pass + hp_check_bytes("C.2.3 lit-never password" as *u8, out, o, exp, 17); tot = tot + 1
425
426 // ---- C.3.1 first request, no Huffman ----
427 // :method GET (82), :scheme http (86), :path / (84),
428 // :authority www.example.com (41 0f + 15 bytes)
429 let auth: *u8 = sys_mmap(32); nx_str_cpy(auth, "www.example.com" as *u8)
430 o = hpack_encode_indexed(out, 0, 2) // 82 :method GET
431 o = hpack_encode_indexed(out, o, 6) // 86 :scheme http
432 o = hpack_encode_indexed(out, o, 4) // 84 :path /
433 o = hpack_encode_lit_inc_indexed_name(out, o, 1, auth, 15) // 41 0f www.example.com
434 // 82 86 84 41 0f 77 77 77 2e 65 78 61 6d 70 6c 65 2e 63 6f 6d
435 exp[0]=0x82 as u8; exp[1]=0x86 as u8; exp[2]=0x84 as u8
436 exp[3]=0x41 as u8; exp[4]=0x0f as u8
437 exp[5]=0x77 as u8; exp[6]=0x77 as u8; exp[7]=0x77 as u8; exp[8]=0x2e as u8
438 exp[9]=0x65 as u8; exp[10]=0x78 as u8; exp[11]=0x61 as u8; exp[12]=0x6d as u8
439 exp[13]=0x70 as u8; exp[14]=0x6c as u8; exp[15]=0x65 as u8; exp[16]=0x2e as u8
440 exp[17]=0x63 as u8; exp[18]=0x6f as u8; exp[19]=0x6d as u8
441 pass = pass + hp_check_bytes("C.3.1 first request" as *u8, out, o, exp, 20); tot = tot + 1
442
443 // ---- static-table lookups ----
444 var sok: i64 = 1
445 if hpack_static_index(":method" as *u8, 7, "GET" as *u8, 3) != 2 { sok = 0 }
446 if hpack_static_index(":path" as *u8, 5, "/" as *u8, 1) != 4 { sok = 0 }
447 if hpack_static_index(":status" as *u8, 7, "200" as *u8, 3) != 8 { sok = 0 }
448 if hpack_static_index(":scheme" as *u8, 7, "https" as *u8, 5) != 7 { sok = 0 }
449 if sok == 1 { hp_puts(" PASS static-table lookups (method=2 path=4 status=8 scheme=7)\n" as *u8) }
450 if sok == 0 { hp_puts(" FAIL static-table lookups\n" as *u8) }
451 pass = pass + sok; tot = tot + 1
452
453 // ---- decode round-trip: re-build C.3.1 then walk it; :status from 88 ----
454 // Re-encode C.3.1 into `out` (o = 20), decode-walk every field; must reach
455 // exactly off==20 with no error.
456 o = hpack_encode_indexed(out, 0, 2)
457 o = hpack_encode_indexed(out, o, 6)
458 o = hpack_encode_indexed(out, o, 4)
459 o = hpack_encode_lit_inc_indexed_name(out, o, 1, auth, 15)
460 let stbox: *i64 = sys_mmap(16) as *i64; stbox[0] = 0
461 var dwalk: i64 = 0
462 var derr: i64 = 0
463 while dwalk < o {
464 let nxt: i64 = hpack_decode_walk_field(out, dwalk, o, stbox)
465 if nxt <= dwalk { derr = 1; dwalk = o }
466 else { dwalk = nxt }
467 }
468 if derr == 0 { if dwalk == o {
469 hp_puts(" PASS decode-walk C.3.1 reaches block end (off=" as *u8); hp_putn(dwalk); hp_puts(")\n" as *u8)
470 pass = pass + 1
471 } }
472 if derr == 1 { hp_puts(" FAIL decode-walk C.3.1 errored\n" as *u8) }
473 tot = tot + 1
474
475 // decode :status 200 from an indexed 0x88
476 out[0] = 0x88 as u8
477 stbox[0] = 0
478 let st_after: i64 = hpack_decode_walk_field(out, 0, 1, stbox)
479 var status_ok: i64 = 0
480 if st_after == 1 { if stbox[0] == 200 { status_ok = 1 } }
481 if status_ok == 1 { hp_puts(" PASS decode :status 200 from 0x88\n" as *u8) }
482 if status_ok == 0 { hp_puts(" FAIL decode :status 200 (got=" as *u8); hp_putn(stbox[0]); hp_puts(")\n" as *u8) }
483 pass = pass + status_ok; tot = tot + 1
484
485 // decode round-trip of an integer (1337) via hpack_decode_int
486 o = hpack_encode_int(out, 0, 5, 0, K_MAGIC_1337)
487 let ibox: *i64 = sys_mmap(16) as *i64
488 let iafter: i64 = hpack_decode_int(out, 0, o, 5, ibox)
489 var int_ok: i64 = 0
490 if iafter == o { if ibox[0] == K_MAGIC_1337 { int_ok = 1 } }
491 if int_ok == 1 { hp_puts(" PASS decode INT round-trip 1337\n" as *u8) }
492 if int_ok == 0 { hp_puts(" FAIL decode INT round-trip (got=" as *u8); hp_putn(ibox[0]); hp_puts(")\n" as *u8) }
493 pass = pass + int_ok; tot = tot + 1
494
495 // ---- TAMPER 1: truncated integer continuation ----
496 // First octet 0x1F (N=5 prefix all-ones) with NO continuation octet in lim.
497 out[0] = 0x1f as u8
498 let tbox: *i64 = sys_mmap(16) as *i64; tbox[0] = -999
499 // lim = 1: the prefix is all-ones so a continuation is required, but off=1
500 // already == lim -> MUST return -1 (truncated), NOT a partial value.
501 let trunc_rc: i64 = hpack_decode_int(out, 0, 1, 5, tbox)
502 var t1ok: i64 = 0
503 if trunc_rc < 0 { t1ok = 1 }
504 if t1ok == 1 { hp_puts(" PASS tamper truncated-int rejected (rc=" as *u8); hp_putn(trunc_rc); hp_puts(")\n" as *u8) }
505 if t1ok == 0 { hp_puts(" FAIL tamper truncated-int NOT rejected (rc=" as *u8); hp_putn(trunc_rc); hp_puts(" val=" as *u8); hp_putn(tbox[0]); hp_puts(")\n" as *u8) }
506 pass = pass + t1ok; tot = tot + 1
507
508 // ---- TAMPER 2: over-long string length (C.2.2 with len byte 0c->ff) ----
509 // Build the C.2.2 frame then corrupt the value-length byte to claim 255
510 // bytes in a 14-byte buffer; hpack_decode_str MUST reject (>lim).
511 o = hpack_encode_lit_noindex_indexed_name(out, 0, 4, spath, 12) // 04 0c 2f ...
512 out[1] = 0xff as u8 // 0c -> ff (claims 127 via 7-bit prefix? 0xff: H=1,len-prefix=0x7f all-ones)
513 // The value string starts at off 1 (after the 04 name-index byte).
514 let sp: *i64 = sys_mmap(16) as *i64
515 let sl: *i64 = sys_mmap(16) as *i64
516 let sh: *i64 = sys_mmap(16) as *i64
517 let str_rc: i64 = hpack_decode_str(out, 1, 14, sp, sl, sh)
518 var t2ok: i64 = 0
519 if str_rc < 0 { t2ok = 1 }
520 if t2ok == 1 { hp_puts(" PASS tamper overlong-string rejected (rc=" as *u8); hp_putn(str_rc); hp_puts(")\n" as *u8) }
521 if t2ok == 0 { hp_puts(" FAIL tamper overlong-string NOT rejected (rc=" as *u8); hp_putn(str_rc); hp_puts(")\n" as *u8) }
522 pass = pass + t2ok; tot = tot + 1
523
524 hp_puts("---- hpack gate: passed " as *u8); hp_putn(pass); hp_puts(" / " as *u8); hp_putn(tot); hp_puts("\n" as *u8)
525 if pass == tot {
526 // DURABLE EVIDENCE (mirror nx_sreach_scorecard.nx:63/112): stdout
527 // evaporates and cannot anchor a row_markers entry -- append ONE status
528 // line of the marker/reconcile-recognised form to knowledge/status/.
529 // Only reached when EVERY KAT + both tamper cases pass (pass == tot).
530 let lfd: i64 = sys_openat_append("knowledge/status/h2_nx_hpack.log" as *u8, 0x1a4)
531 if lfd >= 0 {
532 sys_write(lfd, "R4-H2-002-GATE organ=nx_hpack kats=" as *u8, 35)
533 hp_fdn(lfd, pass); sys_write(lfd, "/" as *u8, 1); hp_fdn(lfd, tot)
534 sys_write(lfd, " tamper=ok verdict=GREEN\n" as *u8, 25)
535 sys_close(lfd)
536 }
537 sys_exit(0)
538 }
539 sys_exit(1)
540 return 0
541}