code wiki / (root) / nx_ipp_codec.nx

nx_ipp_codec.nx source

↩ module page · 457 lines · 19450 B

1// nx_ipp_codec.nx -- sovereign IPP/1.1 wire codec (RFC 8010 encoding) -- R0 keystone of the 2// "better printer standard that works with ANY printer" arc. 3// 4// WHY this is the foundation: 5// IPP (Internet Printing Protocol) / IPP Everywhere is the vendor-neutral standard every modern 6// printer speaks -- it is what AirPrint and CUPS "driverless" printing use under the hood. A Brother, 7// HP, Canon or Epson all answer the SAME Get-Printer-Attributes operation. So a correct IPP codec is 8// the one primitive that lets us manage every printer without a per-vendor driver. This file is the 9// pure byte layer: build a request, parse a response. Transport (TCP/HTTP) is R1; semantics/model 10// (printer-state -> human meaning, waste accounting) is R2+. 11// 12// WHY it kills silent failure (the operator's core complaint): 13// An IPP response carries "printer-state" (idle/processing/stopped) and "printer-state-reasons" -- 14// a 1setOf keyword list that enumerates EVERY reason the device is stuck (media-empty, media-jam, 15// marker-supply-low, cover-open, ...). Brother's own UI swallows these; this parser surfaces them. 16// The parser NEVER returns a silent success on bad bytes: every length is bounds-checked and a 17// malformed message yields the sealed NX_IPP_MALFORMED verdict (no_silent_failure by construction). 18// 19// NEVER-BRICK (global rule #26): this is a pure codec. It performs NO syscalls, touches no hardware, 20// and cannot write firmware/NVRAM. Safe BY CONSTRUCTION -- the safest possible bottom rung. 21// 22// Sovereign: pure integer/byte arithmetic, no imports, no 3rd-party. nx_cc -> nxasm, no gcc. 23// 24// CITED EVIDENCE (fetched by the sovereign researcher over its own TLS-1.3, saved under 25// knowledge/fetched/): 26// ipp_rfc8010.raw RFC 8010 "IPP/1.1: Encoding and Transport" (Jan 2017) 27// - msg = version(2) op/status(2) request-id(4) *attr-group end-tag(0x03) data (sec 3.1.1) 28// - delimiter-tags: operation=0x01 job=0x02 end=0x03 printer=0x04 unsupported=0x05 (Table 2) 29// - value-tags: integer=0x21 boolean=0x22 enum=0x23 keyword=0x44 uri=0x45 charset=0x47 30// naturalLanguage=0x48 mimeMediaType=0x49 nameWithoutLanguage=0x42 (Tables 4-6) 31// - delimiters are 0x00-0x0f, value-tags are 0x10-0xff (sec 3.5.1 -- the walker's key rule) 32// - attr = value-tag(1) name-length(2) name value-length(2) value; lengths are SIGNED-SHORT, 33// big-endian; integer/enum = exactly 4 octets; boolean = 1 octet; 1setOf extra values use 34// name-length 0 "additional-value" (sec 3.1.2, 3.8, 3.9 Table 7) 35// ipp_rfc8011.raw RFC 8011 "IPP/1.1: Model and Semantics" (Jan 2017) 36// - operation-ids (Table): Print-Job=0x0002 Validate-Job=0x0004 Get-Jobs=0x000a 37// Get-Printer-Attributes=0x000b 38// - printer-state (sec 5.4.11): idle=3 processing=4 stopped=5 39// - printer-state-reasons keywords (sec 5.4.12): media-empty media-jam media-needed 40// marker-supply-low marker-supply-empty toner-low toner-empty cover-open door-open 41// input-tray-missing output-area-full spool-area-full ... 42// 43// genealogy_id: project-printer-management-ipp-sclass-2026-06-20 ; reuses the iot-hub discover/classify 44// doctrine (sealed-enum verdicts, no_silent_failure) from nx_iot_announce / nx_iot_classify. 45// license_tier: ORIGINAL 46// 47// nx_capability_claims: 48// needs: [pointer_arithmetic] 49// provides: [ipp_encode_request, ipp_encode_attr, ipp_parse_response, ipp_find_attr, ipp_get_enum] 50// safety: [no_unchecked_deref, no_floating_point, no_syscall, bounded_iteration, 51// never_brick_pure_codec, defensive_bounds_checked] 52// verdict: [sealed_enum_find_verdict, no_silent_failure] 53// license: ORIGINAL 54// kind: printer_runtime_primitive 55// sss: [S6, S7] 56 57// ---- operation-ids (RFC 8011, verified) ---------------------------- 58const NX_IPP_OP_PRINT_JOB: i64 = 0x0002 59const NX_IPP_OP_VALIDATE_JOB: i64 = 0x0004 60const NX_IPP_OP_GET_JOBS: i64 = 0x000a 61const NX_IPP_OP_GET_PRINTER_ATTRIBUTES: i64 = 0x000b 62const NX_IPP_OP_CANCEL_JOB: i64 = 0x0008 63const NX_IPP_OP_GET_JOB_ATTRIBUTES: i64 = 0x0009 64 65// ---- status code (RFC 8011 App. B, verified) ----------------------- 66const NX_IPP_STATUS_OK: i64 = 0x0000 // successful-ok 67 68// ---- delimiter (group) tags (RFC 8010 Table 2) --------------------- 69const NX_IPP_GRP_OPERATION: i64 = 0x01 70const NX_IPP_GRP_JOB: i64 = 0x02 71const NX_IPP_TAG_END: i64 = 0x03 72const NX_IPP_GRP_PRINTER: i64 = 0x04 73const NX_IPP_GRP_UNSUPPORTED: i64 = 0x05 74 75// ---- value tags (RFC 8010 Tables 4-6) ------------------------------ 76const NX_IPP_VT_INTEGER: i64 = 0x21 77const NX_IPP_VT_BOOLEAN: i64 = 0x22 78const NX_IPP_VT_ENUM: i64 = 0x23 79const NX_IPP_VT_KEYWORD: i64 = 0x44 80const NX_IPP_VT_URI: i64 = 0x45 81const NX_IPP_VT_CHARSET: i64 = 0x47 82const NX_IPP_VT_NATLANG: i64 = 0x48 83const NX_IPP_VT_MIMETYPE: i64 = 0x49 84const NX_IPP_VT_NAME: i64 = 0x42 // nameWithoutLanguage 85const NX_IPP_VT_TEXT: i64 = 0x41 // textWithoutLanguage 86 87// ---- printer-state enum (RFC 8011 sec 5.4.11) ---------------------- 88const NX_IPP_PSTATE_IDLE: i64 = 3 89const NX_IPP_PSTATE_PROCESSING: i64 = 4 90const NX_IPP_PSTATE_STOPPED: i64 = 5 91 92// ---- sealed verdict enum (no_silent_failure) ----------------------- 93const NX_IPP_OK: i64 = 1 94const NX_IPP_NOT_FOUND: i64 = 2 95const NX_IPP_MALFORMED: i64 = 3 96const NX_IPP_VERDICT_N: i64 = 4 97 98func nx_ipp_verdict_is_valid(v: i64) -> i64 { 99 if v <= 0 { return 0 } 100 if v >= NX_IPP_VERDICT_N { return 0 } 101 return 1 102} 103 104// ---- byte helpers (network byte order = big-endian) ---------------- 105 106func nx_ipp_strlen(s: *u8) -> i64 { 107 var i: i64 = 0 108 while s[i] != (0 as u8) { i = i + 1 } 109 return i 110} 111 112func nx_ipp_put_u16(buf: *u8, off: i64, v: i64) -> i64 { 113 buf[off] = ((v >> 8) & 0xff) as u8 114 buf[off + 1] = (v & 0xff) as u8 115 return off + 2 116} 117 118func nx_ipp_put_u32(buf: *u8, off: i64, v: i64) -> i64 { 119 buf[off] = ((v >> 24) & 0xff) as u8 120 buf[off + 1] = ((v >> 16) & 0xff) as u8 121 buf[off + 2] = ((v >> 8) & 0xff) as u8 122 buf[off + 3] = (v & 0xff) as u8 123 return off + 4 124} 125 126func nx_ipp_get_u16(buf: *u8, off: i64) -> i64 { 127 let hi: i64 = buf[off] as i64 128 let lo: i64 = buf[off + 1] as i64 129 return ((hi & 0xff) << 8) | (lo & 0xff) 130} 131 132func nx_ipp_get_u32(buf: *u8, off: i64) -> i64 { 133 let b0: i64 = buf[off] as i64 134 let b1: i64 = buf[off + 1] as i64 135 let b2: i64 = buf[off + 2] as i64 136 let b3: i64 = buf[off + 3] as i64 137 return ((b0 & 0xff) << 24) | ((b1 & 0xff) << 16) | ((b2 & 0xff) << 8) | (b3 & 0xff) 138} 139 140func nx_ipp_memcpy(dst: *u8, doff: i64, src: *u8, n: i64) -> i64 { 141 var i: i64 = 0 142 while i < n { 143 dst[doff + i] = src[i] 144 i = i + 1 145 } 146 return doff + n 147} 148 149// compare buf[off .. off+n) against s[0 .. n); 1 if equal, else 0. 150func nx_ipp_name_eq(buf: *u8, off: i64, s: *u8, n: i64) -> i64 { 151 var i: i64 = 0 152 var ok: i64 = 1 153 while i < n { 154 if (buf[off + i] as i64) != (s[i] as i64) { 155 ok = 0 156 i = n 157 } else { 158 i = i + 1 159 } 160 } 161 return ok 162} 163 164// ---- ENCODER ------------------------------------------------------- 165// 166// Write the 8-byte header: version(major,minor) op-id/status(2) request-id(4). Returns next offset (8). 167 168func nx_ipp_begin(buf: *u8, vmaj: i64, vmin: i64, op_or_status: i64, request_id: i64) -> i64 { 169 buf[0] = vmaj as u8 170 buf[1] = vmin as u8 171 var o: i64 = 2 172 o = nx_ipp_put_u16(buf, o, op_or_status) 173 o = nx_ipp_put_u32(buf, o, request_id) 174 return o 175} 176 177// Begin an attribute group (write one delimiter tag). 178func nx_ipp_group(buf: *u8, off: i64, group_tag: i64) -> i64 { 179 buf[off] = group_tag as u8 180 return off + 1 181} 182 183// attribute-with-one-value, generic (string-ish values: charset/uri/keyword/name/text/mimeType). 184func nx_ipp_attr(buf: *u8, off: i64, value_tag: i64, 185 name: *u8, nlen: i64, val: *u8, vlen: i64) -> i64 { 186 var o: i64 = off 187 buf[o] = value_tag as u8 188 o = o + 1 189 o = nx_ipp_put_u16(buf, o, nlen) 190 o = nx_ipp_memcpy(buf, o, name, nlen) 191 o = nx_ipp_put_u16(buf, o, vlen) 192 o = nx_ipp_memcpy(buf, o, val, vlen) 193 return o 194} 195 196// attribute-with-one-value for integer/enum (always exactly 4 octets per RFC 8010 sec 3.8). 197func nx_ipp_attr_int(buf: *u8, off: i64, value_tag: i64, 198 name: *u8, nlen: i64, ival: i64) -> i64 { 199 var o: i64 = off 200 buf[o] = value_tag as u8 201 o = o + 1 202 o = nx_ipp_put_u16(buf, o, nlen) 203 o = nx_ipp_memcpy(buf, o, name, nlen) 204 o = nx_ipp_put_u16(buf, o, 4) 205 o = nx_ipp_put_u32(buf, o, ival) 206 return o 207} 208 209// attribute-with-one-value for boolean (always exactly 1 octet per RFC 8010 sec 3.9 Table 7). 210func nx_ipp_attr_bool(buf: *u8, off: i64, name: *u8, nlen: i64, bval: i64) -> i64 { 211 var o: i64 = off 212 buf[o] = NX_IPP_VT_BOOLEAN as u8 213 o = o + 1 214 o = nx_ipp_put_u16(buf, o, nlen) 215 o = nx_ipp_memcpy(buf, o, name, nlen) 216 o = nx_ipp_put_u16(buf, o, 1) 217 buf[o] = (bval & 0x01) as u8 218 o = o + 1 219 return o 220} 221 222// additional-value for a 1setOf attribute: name-length 0 (RFC 8010 sec 3.1.2). 223func nx_ipp_attr_add(buf: *u8, off: i64, value_tag: i64, val: *u8, vlen: i64) -> i64 { 224 var o: i64 = off 225 buf[o] = value_tag as u8 226 o = o + 1 227 o = nx_ipp_put_u16(buf, o, 0) 228 o = nx_ipp_put_u16(buf, o, vlen) 229 o = nx_ipp_memcpy(buf, o, val, vlen) 230 return o 231} 232 233// Write the mandatory end-of-attributes-tag (0x03). Returns final message length. 234func nx_ipp_end(buf: *u8, off: i64) -> i64 { 235 buf[off] = NX_IPP_TAG_END as u8 236 return off + 1 237} 238 239// ---- header readers ------------------------------------------------ 240 241func nx_ipp_ver_major(buf: *u8) -> i64 { return buf[0] as i64 } 242func nx_ipp_ver_minor(buf: *u8) -> i64 { return buf[1] as i64 } 243func nx_ipp_status(buf: *u8) -> i64 { return nx_ipp_get_u16(buf, 2) } // response status-code 244func nx_ipp_op_id(buf: *u8) -> i64 { return nx_ipp_get_u16(buf, 2) } // request operation-id 245func nx_ipp_request_id(buf: *u8) -> i64 { return nx_ipp_get_u32(buf, 4) } 246 247// ---- DEFENSIVE PARSER (the no-silent-failure walker) --------------- 248// 249// Walk the attribute groups from offset 8, honouring the RFC 8010 rule that tags 0x00-0x0f are group 250// delimiters and 0x10-0xff are value-tags. Find the FIRST attribute named `target`; write its value-tag, 251// value offset and value length through the out-pointers. Returns a sealed verdict: 252// NX_IPP_OK -- found; out_* are valid 253// NX_IPP_NOT_FOUND -- message well-formed but no such attribute 254// NX_IPP_MALFORMED -- a declared length runs past the buffer (truncation/overrun) -> NEVER silent 255// 256// Every length is bounds-checked against n before use. off strictly increases each step, so the loop is 257// bounded by the buffer size. 258 259func nx_ipp_find(buf: *u8, n: i64, target: *u8, 260 out_tag: *i64, out_voff: *i64, out_vlen: *i64) -> i64 { 261 if n < 8 { return NX_IPP_MALFORMED } 262 let tlen: i64 = nx_ipp_strlen(target) 263 var off: i64 = 8 264 var verdict: i64 = NX_IPP_NOT_FOUND 265 var keep: i64 = 1 266 while keep == 1 { 267 if off >= n { 268 keep = 0 269 } else { 270 let tag: i64 = buf[off] as i64 271 if tag == NX_IPP_TAG_END { 272 keep = 0 273 } else { 274 if tag <= 0x0f { 275 // begin-attribute-group delimiter -- step over it 276 off = off + 1 277 } else { 278 // value-tag: tag(1) name-length(2) name(nlen) value-length(2) value(vlen) 279 if (off + 3) > n { 280 verdict = NX_IPP_MALFORMED 281 keep = 0 282 } else { 283 let nlen: i64 = nx_ipp_get_u16(buf, off + 1) 284 let name_off: i64 = off + 3 285 if (name_off + nlen + 2) > n { 286 verdict = NX_IPP_MALFORMED 287 keep = 0 288 } else { 289 let vlen_off: i64 = name_off + nlen 290 let vlen: i64 = nx_ipp_get_u16(buf, vlen_off) 291 let val_off: i64 = vlen_off + 2 292 if (val_off + vlen) > n { 293 verdict = NX_IPP_MALFORMED 294 keep = 0 295 } else { 296 if nlen == tlen { 297 if nx_ipp_name_eq(buf, name_off, target, tlen) == 1 { 298 out_tag[0] = tag 299 out_voff[0] = val_off 300 out_vlen[0] = vlen 301 verdict = NX_IPP_OK 302 keep = 0 303 } 304 } 305 if keep == 1 { off = val_off + vlen } 306 } 307 } 308 } 309 } 310 } 311 } 312 } 313 return verdict 314} 315 316// Convenience: find an integer/enum attribute and decode its 4-octet value. 317// `scratch3` is a caller-provided i64[3] work area (keeps this primitive syscall-free). 318// Validates the value-tag is integer/enum and the length is exactly 4, else NX_IPP_MALFORMED. 319func nx_ipp_get_enum(buf: *u8, n: i64, target: *u8, scratch3: *i64, out_val: *i64) -> i64 { 320 let p_tag: *i64 = scratch3 321 let p_voff: *i64 = ((scratch3 as i64) + 8) as *i64 322 let p_vlen: *i64 = ((scratch3 as i64) + 16) as *i64 323 let v: i64 = nx_ipp_find(buf, n, target, p_tag, p_voff, p_vlen) 324 if v != NX_IPP_OK { return v } 325 let tag: i64 = p_tag[0] 326 if tag != NX_IPP_VT_ENUM { 327 if tag != NX_IPP_VT_INTEGER { return NX_IPP_MALFORMED } 328 } 329 if p_vlen[0] != 4 { return NX_IPP_MALFORMED } 330 out_val[0] = nx_ipp_get_u32(buf, p_voff[0]) 331 return NX_IPP_OK 332} 333 334// signed 32-bit reader (two's complement). IPP integers are signed; marker-levels uses negatives 335// (-1/-2/-3) to mean "unknown" (RFC 3805 Printer MIB semantics), so we must sign-extend. 336func nx_ipp_get_i32(buf: *u8, off: i64) -> i64 { 337 var raw: i64 = nx_ipp_get_u32(buf, off) 338 if raw >= 0x80000000 { raw = raw - 0x100000000 } 339 return raw 340} 341 342// additional integer value for a 1setOf integer/enum attribute (name-length 0, 4-octet value). 343func nx_ipp_attr_add_int(buf: *u8, off: i64, value_tag: i64, ival: i64) -> i64 { 344 var o: i64 = off 345 buf[o] = value_tag as u8 346 o = o + 1 347 o = nx_ipp_put_u16(buf, o, 0) 348 o = nx_ipp_put_u16(buf, o, 4) 349 o = nx_ipp_put_u32(buf, o, ival) 350 return o 351} 352 353// 1 if the 1setOf keyword attribute `name` contains the value `target`; else 0 (absent/malformed -> 0). 354// Walks the named attribute + its additional-values. Bounded + bounds-checked. 355func nx_ipp_set_contains(body: *u8, n: i64, name: *u8, target: *u8) -> i64 { 356 if n < 8 { return 0 } 357 let nm_len: i64 = nx_ipp_strlen(name) 358 let tlen: i64 = nx_ipp_strlen(target) 359 var off: i64 = 8 360 var in_set: i64 = 0 361 var found: i64 = 0 362 var keep: i64 = 1 363 while keep == 1 { 364 if off >= n { keep = 0 } 365 else { 366 let tag: i64 = body[off] as i64 367 if tag == NX_IPP_TAG_END { keep = 0 } 368 else { 369 if tag <= 0x0f { off = off + 1; in_set = 0 } 370 else { 371 if (off + 3) > n { keep = 0 } 372 else { 373 let nlen: i64 = nx_ipp_get_u16(body, off + 1) 374 let name_off: i64 = off + 3 375 if (name_off + nlen + 2) > n { keep = 0 } 376 else { 377 let vlen_off: i64 = name_off + nlen 378 let vlen: i64 = nx_ipp_get_u16(body, vlen_off) 379 let val_off: i64 = vlen_off + 2 380 if (val_off + vlen) > n { keep = 0 } 381 else { 382 if nlen > 0 { 383 if nlen == nm_len { 384 if nx_ipp_name_eq(body, name_off, name, nlen) == 1 { in_set = 1 } 385 else { in_set = 0 } 386 } else { in_set = 0 } 387 } 388 if in_set == 1 { 389 if vlen == tlen { 390 if nx_ipp_name_eq(body, val_off, target, tlen) == 1 { found = 1 } 391 } 392 } 393 off = val_off + vlen 394 } 395 } 396 } 397 } 398 } 399 } 400 } 401 return found 402} 403 404// idx-th (0-based) signed value of a 1setOf integer/enum attribute `name`. Verdict OK/NOT_FOUND/MALFORMED. 405func nx_ipp_set_int_at(body: *u8, n: i64, name: *u8, idx: i64, out_val: *i64) -> i64 { 406 if n < 8 { return NX_IPP_MALFORMED } 407 let nm_len: i64 = nx_ipp_strlen(name) 408 var off: i64 = 8 409 var in_set: i64 = 0 410 var cur: i64 = 0 411 var verdict: i64 = NX_IPP_NOT_FOUND 412 var keep: i64 = 1 413 while keep == 1 { 414 if off >= n { keep = 0 } 415 else { 416 let tag: i64 = body[off] as i64 417 if tag == NX_IPP_TAG_END { keep = 0 } 418 else { 419 if tag <= 0x0f { off = off + 1; in_set = 0 } 420 else { 421 if (off + 3) > n { verdict = NX_IPP_MALFORMED; keep = 0 } 422 else { 423 let nlen: i64 = nx_ipp_get_u16(body, off + 1) 424 let name_off: i64 = off + 3 425 if (name_off + nlen + 2) > n { verdict = NX_IPP_MALFORMED; keep = 0 } 426 else { 427 let vlen_off: i64 = name_off + nlen 428 let vlen: i64 = nx_ipp_get_u16(body, vlen_off) 429 let val_off: i64 = vlen_off + 2 430 if (val_off + vlen) > n { verdict = NX_IPP_MALFORMED; keep = 0 } 431 else { 432 if nlen > 0 { 433 if nlen == nm_len { 434 if nx_ipp_name_eq(body, name_off, name, nlen) == 1 { in_set = 1 } 435 else { in_set = 0 } 436 } else { in_set = 0 } 437 } 438 if in_set == 1 { 439 if cur == idx { 440 if vlen == 4 { 441 out_val[0] = nx_ipp_get_i32(body, val_off) 442 verdict = NX_IPP_OK 443 keep = 0 444 } else { verdict = NX_IPP_MALFORMED; keep = 0 } 445 } 446 cur = cur + 1 447 } 448 if keep == 1 { off = val_off + vlen } 449 } 450 } 451 } 452 } 453 } 454 } 455 } 456 return verdict 457}