gzip_header.nx source
↩ module page · 171 lines · 5637 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
34import "syscalls.nx"
35
36const GZIP_MAGIC_0: i64 = 0x1F
37const GZIP_MAGIC_1: i64 = 0x8B
38const GZIP_METHOD_DEFLATE: i64 = 8
39
40const GZIP_FLG_FTEXT: i64 = 0x01
41const GZIP_FLG_FHCRC: i64 = 0x02
42const GZIP_FLG_FEXTRA: i64 = 0x04
43const GZIP_FLG_FNAME: i64 = 0x08
44const GZIP_FLG_FCOMMENT: i64 = 0x10
45
46const GZIP_ERR_FORMAT: i64 = -1
47const GZIP_ERR_METHOD: i64 = -2
48const GZIP_ERR_SHORT: i64 = -3
49
50struct GzipHeader {
51 method: i64,
52 flg: i64,
53 mtime: i64, // seconds since epoch (0 if unknown)
54 xfl: i64, // 2 = max compression, 4 = fastest
55 os: i64,
56 // Offsets/lengths into the caller's buffer for optional fields.
57 // Zero if absent.
58 name_off: i64, name_len: i64,
59 comment_off: i64, comment_len: i64,
60 extra_off: i64, extra_len: i64,
61 // Total header length (so caller knows where DEFLATE payload starts).
62 header_len: i64,
63}
64
65// Read a little-endian u32.
66func gz_read_u32(buf: *u8, off: i64) -> i64 {
67 let b0: i64 = buf[off]
68 let b1: i64 = buf[off + 1]
69 let b2: i64 = buf[off + 2]
70 let b3: i64 = buf[off + 3]
71 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
72}
73
74// Scan for a NUL terminator starting at off; return length
75// (not counting NUL) or -1 if buffer ends first.
76func gz_scan_nul(buf: *u8, n: i64, off: i64) -> i64 {
77 var i: i64 = off
78 while i < n {
79 if buf[i] == 0 { return i - off }
80 i = i + 1
81 }
82 return -1
83}
84
85func gzip_parse_header(buf: *u8, n: i64, h: *GzipHeader) -> i64 {
86 if n < 10 { return GZIP_ERR_SHORT }
87 if buf[0] != GZIP_MAGIC_0 { return GZIP_ERR_FORMAT }
88 if buf[1] != GZIP_MAGIC_1 { return GZIP_ERR_FORMAT }
89
90 let cm: i64 = buf[2]
91 if cm != GZIP_METHOD_DEFLATE { return GZIP_ERR_METHOD }
92
93 h.method = cm
94 h.flg = buf[3]
95 h.mtime = gz_read_u32(buf, 4)
96 h.xfl = buf[8]
97 h.os = buf[9]
98 h.name_off = 0; h.name_len = 0
99 h.comment_off = 0; h.comment_len = 0
100 h.extra_off = 0; h.extra_len = 0
101
102 var off: i64 = 10
103
104 // FEXTRA: 2-byte LE length then that many bytes of XLEN data.
105 if (h.flg & GZIP_FLG_FEXTRA) != 0 {
106 if n < off + 2 { return GZIP_ERR_SHORT }
107 let xlen: i64 = buf[off] | (buf[off + 1] << 8)
108 off = off + 2
109 if n < off + xlen { return GZIP_ERR_SHORT }
110 h.extra_off = off
111 h.extra_len = xlen
112 off = off + xlen
113 }
114
115 // FNAME: NUL-terminated original filename.
116 if (h.flg & GZIP_FLG_FNAME) != 0 {
117 let nlen: i64 = gz_scan_nul(buf, n, off)
118 if nlen < 0 { return GZIP_ERR_SHORT }
119 h.name_off = off
120 h.name_len = nlen
121 off = off + nlen + 1
122 }
123
124 // FCOMMENT: NUL-terminated comment.
125 if (h.flg & GZIP_FLG_FCOMMENT) != 0 {
126 let clen: i64 = gz_scan_nul(buf, n, off)
127 if clen < 0 { return GZIP_ERR_SHORT }
128 h.comment_off = off
129 h.comment_len = clen
130 off = off + clen + 1
131 }
132
133 // FHCRC: 2 byte header CRC16 (we don't verify).
134 if (h.flg & GZIP_FLG_FHCRC) != 0 {
135 if n < off + 2 { return GZIP_ERR_SHORT }
136 off = off + 2
137 }
138
139 h.header_len = off
140 return 0
141}
142
143// Compile-only smoke: synth a minimal gzip header (no optional
144// flags) and round-trip parse.
145func main() -> i64 {
146 let raw: *u8 = sys_mmap(64)
147 raw[0] = 0x1F; raw[1] = 0x8B; raw[2] = 8
148 raw[3] = 0 // FLG = 0
149 raw[4] = 0x78; raw[5] = 0x56; raw[6] = 0x34; raw[7] = 0x12
150 raw[8] = 0x02 // XFL = max compression
151 raw[9] = 3 // OS = Unix
152
153 let h_raw: *u8 = sys_mmap(128)
154 let h: *GzipHeader = h_raw as *GzipHeader
155 if gzip_parse_header(raw, 10, h) != 0 { return 1 }
156 if h.method != 8 { return 2 }
157 if h.mtime != 0x12345678 { return 3 }
158 if h.xfl != 2 { return 4 }
159 if h.os != 3 { return 5 }
160 if h.header_len != 10 { return 6 }
161
162 // Bad magic => GZIP_ERR_FORMAT.
163 raw[1] = 0x00
164 if gzip_parse_header(raw, 10, h) != GZIP_ERR_FORMAT { return 7 }
165
166 // Bad method => GZIP_ERR_METHOD.
167 raw[1] = 0x8B
168 raw[2] = 5
169 if gzip_parse_header(raw, 10, h) != GZIP_ERR_METHOD { return 8 }
170 return 0
171}