code wiki / (root) / gzip_header.nx

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}