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}