code wiki / (root) / nx_hpack.nx

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}