code wiki / (root) / nx_jpeg_marker.nx

nx_jpeg_marker.nx source

↩ module page · 218 lines · 9180 B

1// nx_jpeg_marker.nx -- JPEG segment marker scanner. Arc C1.1 of the 2// full JPEG decoder per ITU-T Rec. T.81 / ISO 10918-1 (1992). 3// 4// JPEG file structure (T.81 sec B.1): 5// SOI marker (0xFF 0xD8) 6// one or more frame segments: 7// [misc tables: DQT, DHT, DRI, APPn, COM] 8// SOF (Start of Frame: 0xC0 baseline, 0xC2 progressive, etc.) 9// one or more scans: 10// SOS (Start of Scan: 0xFF 0xDA, then header, then entropy-coded segment) 11// [entropy-coded data with 0xFF 0x00 byte-stuffing] 12// EOI marker (0xFF 0xD9) 13// 14// Segment format (variable-length kinds): 0xFF + marker_byte + 2-byte 15// big-endian length (length INCLUDES the 2 length bytes but NOT the 16// 0xFF + marker) + (length-2) payload bytes. 17// 18// Length-less markers: SOI (D8), EOI (D9), TEM (01), RSTn (D0..D7). 19// 20// CRITICAL byte-stuffing rule (T.81 sec F.1.2.3): inside the 21// entropy-coded segment that follows SOS, a literal 0xFF in the 22// compressed bitstream is followed by 0x00. A real marker is 23// 0xFF nn where nn != 0x00 and nn is not in D0..D7 (those are RSTn 24// markers that DO appear inside entropy data as restart points). 25// 26// This primitive scans markers BEFORE the entropy stream gets 27// processed -- it walks from SOI through SOF/DQT/DHT segments and 28// stops at SOS (returning the SOS segment so caller knows where 29// the entropy stream begins). Decoding the entropy stream is the 30// job of nx_jpeg_entropy.nx (next stone). 31// 32// nx_safety_envelope: 33// intended_use: "JPEG marker scanning for the bits-up image 34// decoder pipeline." 35// sil_target: SIL1 36// evidence: [t81_section_b1_canonical_basis, 37// sealed_marker_taxonomy, 38// bounded_iteration] 39// hazard_register: [bug-tape-jpeg-malformed-length-overflow, 40// bug-tape-jpeg-marker-byte-stuffing-mishandled] 41// residual_risk: "Caller MUST bound src_len before invoking; 42// substrate emits MALFORMED on internal bounds 43// failure rather than reading past end." 44// verdict: NOT_YET_EVALUATED 45 46import "nx_syscalls.nx" 47 48// ===== sealed enum: marker class ================================= 49 50const NX_JPEG_M_UNKNOWN: i64 = 0 51const NX_JPEG_M_SOI: i64 = 1 // 0xD8 start-of-image 52const NX_JPEG_M_EOI: i64 = 2 // 0xD9 end-of-image 53const NX_JPEG_M_SOF0: i64 = 3 // 0xC0 baseline DCT 54const NX_JPEG_M_SOF2: i64 = 4 // 0xC2 progressive DCT 55const NX_JPEG_M_SOF_OTHER: i64 = 5 // any other SOFn (extended/diff/lossless) 56const NX_JPEG_M_DHT: i64 = 6 // 0xC4 Huffman table 57const NX_JPEG_M_DQT: i64 = 7 // 0xDB quantization table 58const NX_JPEG_M_DRI: i64 = 8 // 0xDD restart interval 59const NX_JPEG_M_SOS: i64 = 9 // 0xDA start-of-scan 60const NX_JPEG_M_APP: i64 = 10 // 0xE0..0xEF application segments 61const NX_JPEG_M_COM: i64 = 11 // 0xFE comment 62const NX_JPEG_M_RST: i64 = 12 // 0xD0..0xD7 restart markers 63const NX_JPEG_M_OTHER: i64 = 13 // any other valid marker 64const NX_JPEG_M_KIND_N: i64 = 14 65 66func nx_jpeg_m_kind_is_valid(k: i64) -> i64 { 67 if k < 0 { return 0 } 68 if k >= NX_JPEG_M_KIND_N { return 0 } 69 return 1 70} 71 72// Classify the raw marker byte (the byte AFTER 0xFF). 73func nx_jpeg_classify_marker(m: i64) -> i64 { 74 if m == 0xD8 { return NX_JPEG_M_SOI } 75 if m == 0xD9 { return NX_JPEG_M_EOI } 76 if m == 0xC0 { return NX_JPEG_M_SOF0 } 77 if m == 0xC2 { return NX_JPEG_M_SOF2 } 78 if m == 0xC4 { return NX_JPEG_M_DHT } 79 if m == 0xDB { return NX_JPEG_M_DQT } 80 if m == 0xDD { return NX_JPEG_M_DRI } 81 if m == 0xDA { return NX_JPEG_M_SOS } 82 if m == 0xFE { return NX_JPEG_M_COM } 83 // 0xC0..0xCF except CC are SOF markers (CC is DAC, which is 84 // arithmetic coding -- rarely used; lump under SOF_OTHER for now). 85 if m >= 0xC0 { if m <= 0xCF { 86 if m == 0xC4 { return NX_JPEG_M_DHT } // already handled above 87 return NX_JPEG_M_SOF_OTHER 88 } } 89 if m >= 0xE0 { if m <= 0xEF { return NX_JPEG_M_APP } } 90 if m >= 0xD0 { if m <= 0xD7 { return NX_JPEG_M_RST } } 91 return NX_JPEG_M_OTHER 92} 93 94// Returns 1 if the marker is "length-less" (no 2-byte length prefix). 95// Per T.81 sec B.1.1.2: SOI, EOI, TEM, and the eight RSTn markers. 96func nx_jpeg_marker_is_lengthless(m: i64) -> i64 { 97 if m == 0xD8 { return 1 } // SOI 98 if m == 0xD9 { return 1 } // EOI 99 if m == 0x01 { return 1 } // TEM 100 if m >= 0xD0 { if m <= 0xD7 { return 1 } } // RSTn 101 return 0 102} 103 104// ===== segment struct ============================================ 105 106struct NxJpegSegment { 107 marker: i64, // raw marker byte (0xD8 for SOI, etc.) 108 kind: i64, // NX_JPEG_M_* classification 109 seg_off: i64, // file offset of the 0xFF byte 110 seg_len: i64, // total bytes consumed (marker + length + payload) 111 payload_off: i64, // file offset of first payload byte (length-prefixed segments only) 112 payload_len: i64 // payload byte count (excludes 2-byte length prefix) 113} 114 115const NX_JPEG_SEG_BYTES: i64 = 48 116 117// ===== cursor =================================================== 118 119struct NxJpegCursor { 120 src: *u8, 121 src_len: i64, 122 pos: i64 123} 124 125const NX_JPEG_CURSOR_BYTES: i64 = 24 126 127// ===== sealed enum: iteration result ============================ 128 129const NX_JPEG_SEG_OK: i64 = 0 130const NX_JPEG_SEG_EOF: i64 = 1 // cursor reached src_len cleanly 131const NX_JPEG_SEG_MISALIGN: i64 = 2 // expected 0xFF but found something else 132const NX_JPEG_SEG_TRUNC: i64 = 3 // segment length runs past src_len 133const NX_JPEG_SEG_BAD_LEN: i64 = 4 // length field < 2 (must include itself) 134const NX_JPEG_SEG_RESULT_N: i64 = 5 135 136func nx_jpeg_seg_result_is_valid(v: i64) -> i64 { 137 if v < 0 { return 0 } 138 if v >= NX_JPEG_SEG_RESULT_N { return 0 } 139 return 1 140} 141 142// ===== API ====================================================== 143 144func nx_jpeg_seg_init(c: *NxJpegCursor, src: *u8, src_len: i64) -> i64 { 145 c.src = src 146 c.src_len = src_len 147 c.pos = 0 148 return 0 149} 150 151// Walk to the next marker. Skips byte-stuffing pad bytes (0xFF that 152// is not immediately a marker because next byte == 0x00 -- can happen 153// in some encoder outputs at segment boundaries) and 0xFF-fill-bytes 154// (multiple consecutive 0xFF bytes before a marker). 155// 156// Fills `out` and returns NX_JPEG_SEG_OK on success. Returns 157// NX_JPEG_SEG_EOF when cursor reaches src_len with no further markers. 158func nx_jpeg_seg_next(c: *NxJpegCursor, out: *NxJpegSegment) -> i64 { 159 let src: *u8 = c.src 160 let len: i64 = c.src_len 161 var p: i64 = c.pos 162 163 if p >= len { return NX_JPEG_SEG_EOF } 164 165 // Find the marker: scan forward for 0xFF then a non-stuffing byte. 166 while p < len { 167 if src[p] != 0xFF { p = p + 1 } 168 else { 169 // Consume any additional 0xFF fill bytes (T.81 sec B.1.1.2). 170 // Separate scan cursor (scan_q) from result (q) to avoid the 171 // sentinel-clobbering bug class. 172 var q: i64 = len 173 var scan_q: i64 = p + 1 174 while scan_q < len { 175 if src[scan_q] == 0xFF { scan_q = scan_q + 1 } 176 else { q = scan_q; scan_q = len } // break, preserving hit 177 } 178 if q == len { c.pos = len; return NX_JPEG_SEG_EOF } 179 // src[q] is now the marker byte. 180 // Skip stuffing (0xFF 0x00) -- treat as data byte. 181 if src[q] == 0x00 { p = q + 1 } 182 else { 183 // Real marker found at q-1 (0xFF) and q (marker byte). 184 let marker: i64 = src[q] as i64 185 let kind: i64 = nx_jpeg_classify_marker(marker) 186 out.marker = marker 187 out.kind = kind 188 out.seg_off = q - 1 189 out.payload_off = 0 190 out.payload_len = 0 191 192 if nx_jpeg_marker_is_lengthless(marker) == 1 { 193 out.seg_len = 2 194 c.pos = q + 1 195 return NX_JPEG_SEG_OK 196 } 197 198 // Length-prefixed segment: 2-byte big-endian length 199 // immediately after the marker byte. Length INCLUDES 200 // those 2 bytes. 201 if q + 2 >= len { c.pos = len; return NX_JPEG_SEG_TRUNC } 202 let hi: i64 = src[q + 1] as i64 203 let lo: i64 = src[q + 2] as i64 204 let seg_field_len: i64 = (hi << 8) | lo 205 if seg_field_len < 2 { c.pos = len; return NX_JPEG_SEG_BAD_LEN } 206 let payload_off: i64 = q + 3 207 let payload_len: i64 = seg_field_len - 2 208 if payload_off + payload_len > len { c.pos = len; return NX_JPEG_SEG_TRUNC } 209 out.payload_off = payload_off 210 out.payload_len = payload_len 211 out.seg_len = 2 + seg_field_len // 0xFF + marker + length-prefix-and-payload 212 c.pos = payload_off + payload_len 213 return NX_JPEG_SEG_OK 214 } 215 } 216 } 217 return NX_JPEG_SEG_EOF 218}