code wiki / (root) / nx_gzip_header.nx

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}