nx_gzip_header.nx source
↩ module page · 177 lines · 5720 B
1// gzip_header.nx -- parse RFC 1952 gzip stream headers.
2//
3// gzip = DEFLATE (RFC 1951) + gzip framing (RFC 1952). This
4// module parses the framing, not the compression; a future
5// deflate.nx will handle block decompression.
6//
7// Use cases: inspect a .gz file's original filename + mtime,
8// detect corruption before paying the decompress cost, split a
9// multi-member concatenated archive (gzip supports that), read
10// NAS backup archives.
11//
12// Header layout (fixed-prefix then variable optionals):
13// ID1 1 byte = 0x1F
14// ID2 1 byte = 0x8B
15// CM 1 byte compression method; 8 = DEFLATE
16// FLG 1 byte flag bits
17// MTIME 4 bytes modification time, seconds since epoch LE
18// XFL 1 byte extra flags
19// OS 1 byte originating filesystem
20//
21// FLG bits:
22// bit 0 FTEXT probably ASCII text
23// bit 1 FHCRC header CRC16 follows the header
24// bit 2 FEXTRA extra fields follow (2-byte length + data)
25// bit 3 FNAME original filename follows (NUL-terminated)
26// bit 4 FCOMMENT comment follows (NUL-terminated)
27// bits 5-7 reserved
28//
29// Invariants:
30// G1 Magic (0x1F 0x8B) must match.
31// G2 CM must be 8 (no other method is defined).
32// G3 Unterminated FNAME / FCOMMENT returns GZIP_ERR_SHORT.
33
34// nx_safety_envelope:
35// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
36// sil_target: SIL1
37// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
38// verdict: NOT_YET_EVALUATED
39
40import "nx_syscalls.nx"
41
42const GZIP_MAGIC_0: i64 = 0x1F
43const GZIP_MAGIC_1: i64 = 0x8B
44const GZIP_METHOD_DEFLATE: i64 = 8
45
46const GZIP_FLG_FTEXT: i64 = 0x01
47const GZIP_FLG_FHCRC: i64 = 0x02
48const GZIP_FLG_FEXTRA: i64 = 0x04
49const GZIP_FLG_FNAME: i64 = 0x08
50const GZIP_FLG_FCOMMENT: i64 = 0x10
51
52const GZIP_ERR_FORMAT: i64 = -1
53const GZIP_ERR_METHOD: i64 = -2
54const GZIP_ERR_SHORT: i64 = -3
55
56struct GzipHeader {
57 method: i64,
58 flg: i64,
59 mtime: i64, // seconds since epoch (0 if unknown)
60 xfl: i64, // 2 = max compression, 4 = fastest
61 os: i64,
62 // Offsets/lengths into the caller's buffer for optional fields.
63 // Zero if absent.
64 name_off: i64, name_len: i64,
65 comment_off: i64, comment_len: i64,
66 extra_off: i64, extra_len: i64,
67 // Total header length (so caller knows where DEFLATE payload starts).
68 header_len: i64,
69}
70
71// Read a little-endian u32.
72func gz_read_u32(buf: *u8, off: i64) -> i64 {
73 let b0: i64 = buf[off]
74 let b1: i64 = buf[off + 1]
75 let b2: i64 = buf[off + 2]
76 let b3: i64 = buf[off + 3]
77 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
78}
79
80// Scan for a NUL terminator starting at off; return length
81// (not counting NUL) or -1 if buffer ends first.
82func gz_scan_nul(buf: *u8, n: i64, off: i64) -> i64 {
83 var i: i64 = off
84 while i < n {
85 if buf[i] == 0 { return i - off }
86 i = i + 1
87 }
88 return -1
89}
90
91func gzip_parse_header(buf: *u8, n: i64, h: *GzipHeader) -> i64 {
92 if n < 10 { return GZIP_ERR_SHORT }
93 if buf[0] != GZIP_MAGIC_0 { return GZIP_ERR_FORMAT }
94 if buf[1] != GZIP_MAGIC_1 { return GZIP_ERR_FORMAT }
95
96 let cm: i64 = buf[2]
97 if cm != GZIP_METHOD_DEFLATE { return GZIP_ERR_METHOD }
98
99 h.method = cm
100 h.flg = buf[3]
101 h.mtime = gz_read_u32(buf, 4)
102 h.xfl = buf[8]
103 h.os = buf[9]
104 h.name_off = 0; h.name_len = 0
105 h.comment_off = 0; h.comment_len = 0
106 h.extra_off = 0; h.extra_len = 0
107
108 var off: i64 = 10
109
110 // FEXTRA: 2-byte LE length then that many bytes of XLEN data.
111 if (h.flg & GZIP_FLG_FEXTRA) != 0 {
112 if n < off + 2 { return GZIP_ERR_SHORT }
113 let xlen: i64 = buf[off] | (buf[off + 1] << 8)
114 off = off + 2
115 if n < off + xlen { return GZIP_ERR_SHORT }
116 h.extra_off = off
117 h.extra_len = xlen
118 off = off + xlen
119 }
120
121 // FNAME: NUL-terminated original filename.
122 if (h.flg & GZIP_FLG_FNAME) != 0 {
123 let nlen: i64 = gz_scan_nul(buf, n, off)
124 if nlen < 0 { return GZIP_ERR_SHORT }
125 h.name_off = off
126 h.name_len = nlen
127 off = off + nlen + 1
128 }
129
130 // FCOMMENT: NUL-terminated comment.
131 if (h.flg & GZIP_FLG_FCOMMENT) != 0 {
132 let clen: i64 = gz_scan_nul(buf, n, off)
133 if clen < 0 { return GZIP_ERR_SHORT }
134 h.comment_off = off
135 h.comment_len = clen
136 off = off + clen + 1
137 }
138
139 // FHCRC: 2 byte header CRC16 (we don't verify).
140 if (h.flg & GZIP_FLG_FHCRC) != 0 {
141 if n < off + 2 { return GZIP_ERR_SHORT }
142 off = off + 2
143 }
144
145 h.header_len = off
146 return 0
147}
148
149// Compile-only smoke: synth a minimal gzip header (no optional
150// flags) and round-trip parse.
151func main() -> i64 {
152 let raw: *u8 = sys_mmap(64)
153 raw[0] = 0x1F; raw[1] = 0x8B; raw[2] = 8
154 raw[3] = 0 // FLG = 0
155 raw[4] = 0x78; raw[5] = 0x56; raw[6] = 0x34; raw[7] = 0x12
156 raw[8] = 0x02 // XFL = max compression
157 raw[9] = 3 // OS = Unix
158
159 let h_raw: *u8 = sys_mmap(128)
160 let h: *GzipHeader = h_raw as *GzipHeader
161 if gzip_parse_header(raw, 10, h) != 0 { return 1 }
162 if h.method != 8 { return 2 }
163 if h.mtime != 0x12345678 { return 3 }
164 if h.xfl != 2 { return 4 }
165 if h.os != 3 { return 5 }
166 if h.header_len != 10 { return 6 }
167
168 // Bad magic => GZIP_ERR_FORMAT.
169 raw[1] = 0x00
170 if gzip_parse_header(raw, 10, h) != GZIP_ERR_FORMAT { return 7 }
171
172 // Bad method => GZIP_ERR_METHOD.
173 raw[1] = 0x8B
174 raw[2] = 5
175 if gzip_parse_header(raw, 10, h) != GZIP_ERR_METHOD { return 8 }
176 return 0
177}