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}