nx_gzip_wrap.nx source
↩ module page · 379 lines · 12845 B
1// nx_gzip_wrap.nx -- RFC 1952 gzip container (DEFLATE + magic + CRC32 + size).
2//
3// CAPABILITY_COMPLETENESS: FULL
4//
5// gzip stream layout per RFC 1952:
6//
7// +---+---+---+---+---+---+---+---+---+---+
8// |ID1|ID2|CM |FLG| MTIME |XFL|OS | 10-byte fixed header
9// +---+---+---+---+---+---+---+---+---+---+
10// [ XLEN | <extra field> ] if FLG.FEXTRA set
11// [ FNAME (zero-terminated) ] if FLG.FNAME set
12// [ FCOMMENT (zero-terminated) ] if FLG.FCOMMENT set
13// [ CRC16-of-header-so-far ] if FLG.FHCRC set
14// +-----------------------------+
15// | ... | DEFLATE-compressed data
16// +-----------------------------+
17// | CRC32 | 4-byte LE CRC-32 of UNCOMPRESSED
18// +-----------------------------+
19// | ISIZE | 4-byte LE uncompressed size mod 2^32
20// +-----------------------------+
21//
22// FLG bits:
23// 0 FTEXT -- ASCII hint (advisory)
24// 1 FHCRC -- 2-byte header CRC16 trailer present
25// 2 FEXTRA -- 2-byte XLEN + extra field present
26// 3 FNAME -- zero-terminated filename present
27// 4 FCOMMENT -- zero-terminated comment present
28// 5-7 -- reserved; must be 0 (BAD_FLAGS otherwise)
29//
30// CM must be 8 (deflate) -- only legal value per spec.
31//
32// genealogy_id: rfc1952_gzip_1996
33// lineage_id: nx_gzip_wrap_v1
34//
35// Composes: nx_deflate + nx_crc32 (already shipped).
36
37// nx_safety_envelope:
38// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
39// sil_target: SIL1
40// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
41// verdict: NOT_YET_EVALUATED
42
43import "nx_syscalls.nx"
44import "nx_runtime.nx"
45import "nx_tier.nx"
46import "nx_deflate.nx"
47import "nx_crc32.nx"
48const NX_MAGIC_4294967295: i64 = 4294967295
49
50// ===== error codes ================================================
51
52const NX_GZ_OK: nx_int = 0
53const NX_GZ_ERR_TOO_SHORT: nx_int = 1
54const NX_GZ_ERR_BAD_MAGIC: nx_int = 2
55const NX_GZ_ERR_BAD_METHOD: nx_int = 3
56const NX_GZ_ERR_BAD_FLAGS: nx_int = 4
57const NX_GZ_ERR_FIELD_OVERRUN: nx_int = 5
58const NX_GZ_ERR_DEFLATE: nx_int = 6
59const NX_GZ_ERR_CRC_MISMATCH: nx_int = 7
60const NX_GZ_ERR_SIZE_MISMATCH: nx_int = 8
61
62// FLG bits.
63const NX_GZ_FTEXT: nx_int = 1 // bit 0
64const NX_GZ_FHCRC: nx_int = 2 // bit 1
65const NX_GZ_FEXTRA: nx_int = 4 // bit 2
66const NX_GZ_FNAME: nx_int = 8 // bit 3
67const NX_GZ_FCOMMENT: nx_int = 16 // bit 4
68const NX_GZ_FRESERVED: nx_int = 224 // bits 5-7
69
70// ===== result struct ==============================================
71
72struct NxGzipResult {
73 output_data: *u8,
74 output_size: nx_int,
75 bytes_consumed: nx_int,
76 error_code: nx_int,
77 mtime: nx_int,
78 xfl: nx_int,
79 os_id: nx_int,
80 crc_expected: nx_int,
81 crc_computed: nx_int,
82 size_expected: nx_int,
83 size_actual: nx_int,
84}
85
86const NX_GZ_RESULT_BYTES: nx_size = 96
87
88// ===== read LE 16/32 helpers ======================================
89
90func _gz_read_u16_le(buf: *u8, off: nx_int) -> nx_int {
91 let b0: nx_int = (buf[off] as nx_int) & 255
92 let b1: nx_int = (buf[off + 1] as nx_int) & 255
93 return b0 | (b1 << 8)
94}
95
96func _gz_read_u32_le(buf: *u8, off: nx_int) -> nx_int {
97 let b0: nx_int = (buf[off] as nx_int) & 255
98 let b1: nx_int = (buf[off + 1] as nx_int) & 255
99 let b2: nx_int = (buf[off + 2] as nx_int) & 255
100 let b3: nx_int = (buf[off + 3] as nx_int) & 255
101 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
102}
103
104// Skip a zero-terminated string starting at off; returns new offset
105// pointing JUST PAST the NUL, or -1 on overrun.
106func _gz_skip_cstring(buf: *u8, off: nx_int, end: nx_int) -> nx_int {
107 var p: nx_int = off
108 while p < end {
109 if buf[p] == (0 as u8) { return p + 1 }
110 p = p + 1
111 }
112 return -1
113}
114
115// ===== inflate full gzip stream ===================================
116
117func nx_gzip_inflate(input: *u8, input_size: nx_int,
118 max_output: nx_int) -> *NxGzipResult {
119 let r_ptr: *u8 = sys_mmap(NX_GZ_RESULT_BYTES)
120 let r: *NxGzipResult = r_ptr as *NxGzipResult
121 r.output_data = 0 as *u8
122 r.output_size = 0
123 r.bytes_consumed = 0
124 r.error_code = NX_GZ_OK
125
126 if input_size < 18 { // 10-byte header + min 0-byte DEFLATE + 8-byte trailer
127 r.error_code = NX_GZ_ERR_TOO_SHORT
128 return r
129 }
130
131 let id1: nx_int = (input[0] as nx_int) & 255
132 let id2: nx_int = (input[1] as nx_int) & 255
133 if id1 != 31 {
134 r.error_code = NX_GZ_ERR_BAD_MAGIC
135 return r
136 }
137 if id2 != 139 {
138 r.error_code = NX_GZ_ERR_BAD_MAGIC
139 return r
140 }
141 let cm: nx_int = (input[2] as nx_int) & 255
142 if cm != 8 {
143 r.error_code = NX_GZ_ERR_BAD_METHOD
144 return r
145 }
146 let flg: nx_int = (input[3] as nx_int) & 255
147 if (flg & NX_GZ_FRESERVED) != 0 {
148 r.error_code = NX_GZ_ERR_BAD_FLAGS
149 return r
150 }
151 r.mtime = _gz_read_u32_le(input, 4)
152 r.xfl = (input[8] as nx_int) & 255
153 r.os_id = (input[9] as nx_int) & 255
154
155 // Walk optional header fields.
156 var off: nx_int = 10
157 if (flg & NX_GZ_FEXTRA) != 0 {
158 if (off + 2) > input_size {
159 r.error_code = NX_GZ_ERR_FIELD_OVERRUN
160 return r
161 }
162 let xlen: nx_int = _gz_read_u16_le(input, off)
163 off = off + 2 + xlen
164 if off > input_size {
165 r.error_code = NX_GZ_ERR_FIELD_OVERRUN
166 return r
167 }
168 }
169 if (flg & NX_GZ_FNAME) != 0 {
170 let next_off: nx_int = _gz_skip_cstring(input, off, input_size)
171 if next_off < 0 {
172 r.error_code = NX_GZ_ERR_FIELD_OVERRUN
173 return r
174 }
175 off = next_off
176 }
177 if (flg & NX_GZ_FCOMMENT) != 0 {
178 let next_off2: nx_int = _gz_skip_cstring(input, off, input_size)
179 if next_off2 < 0 {
180 r.error_code = NX_GZ_ERR_FIELD_OVERRUN
181 return r
182 }
183 off = next_off2
184 }
185 if (flg & NX_GZ_FHCRC) != 0 {
186 // Header CRC16 trailer present; substrate validates the
187 // CRC16 over preceding bytes if nx_crc16 is available.
188 // For v1 we trust + skip (gzip-CRC16 is rarely set in
189 // mainstream output; if needed compose against nx_crc16).
190 if (off + 2) > input_size {
191 r.error_code = NX_GZ_ERR_FIELD_OVERRUN
192 return r
193 }
194 off = off + 2
195 }
196
197 // DEFLATE payload runs from off to input_size - 8 (CRC32 + ISIZE).
198 let payload_size: nx_int = input_size - off - 8
199 if payload_size < 0 {
200 r.error_code = NX_GZ_ERR_TOO_SHORT
201 return r
202 }
203 let payload: *u8 = (input as nx_int + off) as *u8
204
205 let def_r: *NxDeflateResult = nx_deflate_inflate(
206 payload, payload_size, max_output)
207 if def_r == (0 as *NxDeflateResult) {
208 r.error_code = NX_GZ_ERR_DEFLATE
209 return r
210 }
211 if def_r.error_code != NX_DEF_OK {
212 r.error_code = NX_GZ_ERR_DEFLATE
213 r.output_data = def_r.output_data
214 r.output_size = def_r.output_size
215 r.bytes_consumed = off + def_r.bytes_consumed
216 return r
217 }
218
219 let trailer_off: nx_int = off + def_r.bytes_consumed
220 if (trailer_off + 8) > input_size {
221 r.error_code = NX_GZ_ERR_TOO_SHORT
222 return r
223 }
224 let crc_expected: nx_int = _gz_read_u32_le(input, trailer_off)
225 let size_expected: nx_int = _gz_read_u32_le(input, trailer_off + 4)
226 let crc_computed: nx_int = nx_crc32(def_r.output_data, def_r.output_size)
227 let size_actual: nx_int = def_r.output_size
228
229 r.crc_expected = crc_expected
230 r.crc_computed = crc_computed
231 r.size_expected = size_expected
232 r.size_actual = size_actual
233
234 if crc_expected != crc_computed {
235 r.error_code = NX_GZ_ERR_CRC_MISMATCH
236 } else {
237 // ISIZE is uncompressed size mod 2^32.
238 let mask32: nx_int = NX_MAGIC_4294967295 // 0xFFFFFFFF
239 let actual_mod: nx_int = size_actual & mask32
240 if actual_mod != size_expected {
241 r.error_code = NX_GZ_ERR_SIZE_MISMATCH
242 }
243 }
244
245 r.output_data = def_r.output_data
246 r.output_size = def_r.output_size
247 r.bytes_consumed = trailer_off + 8
248 return r
249}
250
251// ===== self-test ==================================================
252//
253// Construct a minimal gzip stream containing "Hi".
254// ID1 = 0x1F, ID2 = 0x8B, CM = 0x08, FLG = 0x00
255// MTIME = 0x00000000, XFL = 0x00, OS = 0xFF (unknown)
256// DEFLATE payload (stored block "Hi"):
257// 0x01 0x02 0x00 0xFD 0xFF 'H' 'i'
258// CRC-32 of "Hi" (little-endian)
259// ISIZE = 2 (little-endian)
260
261func main() -> nx_int {
262 // Compute CRC-32 of "Hi" first.
263 let hi: *u8 = (sys_mmap(2)) as *u8
264 hi[0] = 0x48 as u8
265 hi[1] = 0x69 as u8
266 let crc_hi: nx_int = nx_crc32(hi, 2)
267 // CRC-32 of "Hi" is computed at runtime via nx_crc32 (validated
268 // in nx_crc32.nx self-test for "123456789" -> 0xCBF43926). Do
269 // NOT hardcode an expected literal here -- previously the comment
270 // claimed 0xD8932AAD which is wrong (real low-byte is 0x0E per
271 // nx_bgzf_test.nx round-trip). Computing it inline keeps this
272 // self-test honest.
273
274 let stream: *u8 = (sys_mmap(25)) as *u8
275 stream[0] = 31 as u8 // ID1
276 stream[1] = 139 as u8 // ID2
277 stream[2] = 8 as u8 // CM = deflate
278 stream[3] = 0 as u8 // FLG = none
279 stream[4] = 0 as u8 // MTIME
280 stream[5] = 0 as u8
281 stream[6] = 0 as u8
282 stream[7] = 0 as u8
283 stream[8] = 0 as u8 // XFL
284 stream[9] = 255 as u8 // OS = unknown
285 // DEFLATE stored block carrying 'H' 'i'
286 stream[10] = 1 as u8 // BFINAL=1 BTYPE=00
287 stream[11] = 2 as u8 // LEN lo
288 stream[12] = 0 as u8 // LEN hi
289 stream[13] = 253 as u8 // NLEN lo (0xFD)
290 stream[14] = 255 as u8 // NLEN hi (0xFF)
291 stream[15] = 0x48 as u8 // 'H'
292 stream[16] = 0x69 as u8 // 'i'
293 // CRC-32 of "Hi" in little-endian.
294 let crc_b0: nx_int = crc_hi & 255
295 let crc_b1: nx_int = (crc_hi >> 8) & 255
296 let crc_b2: nx_int = (crc_hi >> 16) & 255
297 let crc_b3: nx_int = (crc_hi >> 24) & 255
298 stream[17] = crc_b0 as u8
299 stream[18] = crc_b1 as u8
300 stream[19] = crc_b2 as u8
301 stream[20] = crc_b3 as u8
302 // ISIZE = 2 little-endian.
303 stream[21] = 2 as u8
304 stream[22] = 0 as u8
305 stream[23] = 0 as u8
306 stream[24] = 0 as u8
307
308 let r: *NxGzipResult = nx_gzip_inflate(stream, 25, 64)
309 if r == (0 as *NxGzipResult) { return 1 }
310 if r.error_code != NX_GZ_OK { return 2 }
311 if r.output_size != 2 { return 3 }
312 if r.output_data[0] != (0x48 as u8) { return 4 }
313 if r.output_data[1] != (0x69 as u8) { return 5 }
314 if r.os_id != 255 { return 6 }
315 if r.size_expected != 2 { return 7 }
316 if r.crc_expected != r.crc_computed { return 8 }
317
318 // ---- BAD_MAGIC ----
319 let bad_magic: *u8 = (sys_mmap(25)) as *u8
320 var i: nx_int = 0
321 while i < 25 {
322 bad_magic[i] = stream[i]
323 i = i + 1
324 }
325 bad_magic[0] = 0x00 as u8
326 let rbm: *NxGzipResult = nx_gzip_inflate(bad_magic, 25, 64)
327 if rbm.error_code != NX_GZ_ERR_BAD_MAGIC { return 20 }
328
329 // ---- BAD_METHOD ----
330 let bad_method: *u8 = (sys_mmap(25)) as *u8
331 var j: nx_int = 0
332 while j < 25 {
333 bad_method[j] = stream[j]
334 j = j + 1
335 }
336 bad_method[2] = 9 as u8
337 let rbme: *NxGzipResult = nx_gzip_inflate(bad_method, 25, 64)
338 if rbme.error_code != NX_GZ_ERR_BAD_METHOD { return 30 }
339
340 // ---- BAD_FLAGS (reserved bit 5 set) ----
341 let bad_flags: *u8 = (sys_mmap(25)) as *u8
342 var k: nx_int = 0
343 while k < 25 {
344 bad_flags[k] = stream[k]
345 k = k + 1
346 }
347 bad_flags[3] = 0x20 as u8 // bit 5 reserved
348 let rbf: *NxGzipResult = nx_gzip_inflate(bad_flags, 25, 64)
349 if rbf.error_code != NX_GZ_ERR_BAD_FLAGS { return 40 }
350
351 // ---- CRC_MISMATCH ----
352 let bad_crc: *u8 = (sys_mmap(25)) as *u8
353 var m: nx_int = 0
354 while m < 25 {
355 bad_crc[m] = stream[m]
356 m = m + 1
357 }
358 bad_crc[17] = (stream[17] as nx_int ^ 0xFF) as u8
359 let rbc: *NxGzipResult = nx_gzip_inflate(bad_crc, 25, 64)
360 if rbc.error_code != NX_GZ_ERR_CRC_MISMATCH { return 50 }
361
362 // ---- SIZE_MISMATCH ----
363 let bad_size: *u8 = (sys_mmap(25)) as *u8
364 var n: nx_int = 0
365 while n < 25 {
366 bad_size[n] = stream[n]
367 n = n + 1
368 }
369 bad_size[21] = 99 as u8 // wrong ISIZE
370 let rbs: *NxGzipResult = nx_gzip_inflate(bad_size, 25, 64)
371 if rbs.error_code != NX_GZ_ERR_SIZE_MISMATCH { return 60 }
372
373 // ---- TOO_SHORT ----
374 let too_short: *u8 = (sys_mmap(8)) as *u8
375 let rts: *NxGzipResult = nx_gzip_inflate(too_short, 8, 64)
376 if rts.error_code != NX_GZ_ERR_TOO_SHORT { return 70 }
377
378 return 0
379}