zip_header.nx source
↩ module page · 203 lines · 7611 B
1// zip_header.nx -- parse PKZIP APPNOTE 6.3.x framing.
2//
3// Parses two key structures in a .zip archive:
4// 1. Local File Header (LFH) -- one per stored file, prefixes
5// each compressed payload. Signature 0x04034b50.
6// 2. End of Central Directory record (EOCD) -- at the tail of
7// the archive; points to the central directory. Signature
8// 0x06054b50.
9//
10// This module does NOT parse the central directory entries (CDFH)
11// or decompress payloads. With just LFH + EOCD the Nishi stack
12// can: detect zip framing, count members, read filenames,
13// identify compression method (0=stored, 8=DEFLATE, 14=LZMA,
14// 93=Zstd...), and find the offset of each file's raw bytes.
15//
16// All multi-byte values in ZIP are little-endian.
17//
18// Invariants:
19// Z1 LFH signature must be 0x04034b50.
20// Z2 EOCD signature must be 0x06054b50 (may be preceded by a
21// variable-length ZIP comment; caller scans backward).
22// Z3 Filenames are stored raw; if bit 11 of general-purpose
23// flag is set, they are UTF-8; otherwise CP437. We expose
24// offset+length and let caller decide.
25// Z4 Compressed-size / uncompressed-size may be 0xFFFFFFFF to
26// indicate Zip64 extensions; we surface that as a sentinel
27// for now.
28
29import "syscalls.nx"
30
31const ZIP_LFH_SIG: i64 = 0x04034b50
32const ZIP_EOCD_SIG: i64 = 0x06054b50
33
34const ZIP_ERR_FORMAT: i64 = -1
35const ZIP_ERR_SHORT: i64 = -2
36
37const ZIP64_MARKER: i64 = 0xFFFFFFFF
38
39struct ZipLocalHeader {
40 version_needed: i64,
41 flag: i64,
42 method: i64, // 0 stored, 8 deflate, 14 lzma, 93 zstd
43 mod_time: i64, // MS-DOS time
44 mod_date: i64, // MS-DOS date
45 crc32: i64,
46 compressed_size: i64,
47 uncompressed_size: i64,
48 name_off: i64, name_len: i64,
49 extra_off: i64, extra_len: i64,
50 // Header length so caller knows where the compressed payload starts.
51 header_len: i64,
52}
53
54struct ZipEndOfCentralDir {
55 disk_number: i64,
56 cd_start_disk: i64,
57 entries_this_disk: i64,
58 entries_total: i64,
59 cd_size: i64,
60 cd_offset: i64,
61 comment_off: i64,
62 comment_len: i64,
63}
64
65// Little-endian reads.
66func zip_read_u16(buf: *u8, off: i64) -> i64 {
67 return buf[off] | (buf[off + 1] << 8)
68}
69
70func zip_read_u32(buf: *u8, off: i64) -> i64 {
71 let b0: i64 = buf[off]
72 let b1: i64 = buf[off + 1]
73 let b2: i64 = buf[off + 2]
74 let b3: i64 = buf[off + 3]
75 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
76}
77
78// Parse a Local File Header at buf[off]. Returns 0 on success
79// or a negative ZIP_ERR_*.
80func zip_parse_lfh(buf: *u8, n: i64, off: i64, h: *ZipLocalHeader) -> i64 {
81 if n < off + 30 { return ZIP_ERR_SHORT }
82 if zip_read_u32(buf, off) != ZIP_LFH_SIG { return ZIP_ERR_FORMAT }
83
84 h.version_needed = zip_read_u16(buf, off + 4)
85 h.flag = zip_read_u16(buf, off + 6)
86 h.method = zip_read_u16(buf, off + 8)
87 h.mod_time = zip_read_u16(buf, off + 10)
88 h.mod_date = zip_read_u16(buf, off + 12)
89 h.crc32 = zip_read_u32(buf, off + 14)
90 h.compressed_size = zip_read_u32(buf, off + 18)
91 h.uncompressed_size = zip_read_u32(buf, off + 22)
92 let name_len: i64 = zip_read_u16(buf, off + 26)
93 let extra_len: i64 = zip_read_u16(buf, off + 28)
94
95 let name_off: i64 = off + 30
96 if n < name_off + name_len + extra_len { return ZIP_ERR_SHORT }
97 h.name_off = name_off
98 h.name_len = name_len
99 h.extra_off = name_off + name_len
100 h.extra_len = extra_len
101 h.header_len = 30 + name_len + extra_len
102 return 0
103}
104
105// Scan backwards from end-of-buffer for the EOCD signature. ZIP
106// allows a trailing comment up to 65535 bytes, so we bound the
107// scan to the last 66000 bytes. Returns offset of EOCD or -1.
108func zip_find_eocd(buf: *u8, n: i64) -> i64 {
109 var limit: i64 = n - 66000
110 if limit < 0 { limit = 0 }
111 // EOCD is at least 22 bytes; last possible start = n-22.
112 var off: i64 = n - 22
113 while off >= limit {
114 if off + 3 < n {
115 if zip_read_u32(buf, off) == ZIP_EOCD_SIG { return off }
116 }
117 off = off - 1
118 }
119 return -1
120}
121
122// Parse EOCD at given offset.
123func zip_parse_eocd(buf: *u8, n: i64, off: i64,
124 e: *ZipEndOfCentralDir) -> i64 {
125 if n < off + 22 { return ZIP_ERR_SHORT }
126 if zip_read_u32(buf, off) != ZIP_EOCD_SIG { return ZIP_ERR_FORMAT }
127
128 e.disk_number = zip_read_u16(buf, off + 4)
129 e.cd_start_disk = zip_read_u16(buf, off + 6)
130 e.entries_this_disk = zip_read_u16(buf, off + 8)
131 e.entries_total = zip_read_u16(buf, off + 10)
132 e.cd_size = zip_read_u32(buf, off + 12)
133 e.cd_offset = zip_read_u32(buf, off + 16)
134 let comment_len: i64 = zip_read_u16(buf, off + 20)
135 if n < off + 22 + comment_len { return ZIP_ERR_SHORT }
136 e.comment_off = off + 22
137 e.comment_len = comment_len
138 return 0
139}
140
141// Helper: map compression method code to a short name length.
142// Returns 0 if unknown. Used for debug printing; callers print
143// from a static table.
144func zip_method_known(method: i64) -> i64 {
145 if method == 0 { return 1 } // stored
146 if method == 8 { return 1 } // deflate
147 if method == 14 { return 1 } // lzma
148 if method == 93 { return 1 } // zstd
149 if method == 95 { return 1 } // xz
150 if method == 98 { return 1 } // ppmd
151 return 0
152}
153
154// Compile-only smoke: forge minimal LFH + EOCD in a buffer.
155func main() -> i64 {
156 let raw: *u8 = sys_mmap(256)
157 var i: i64 = 0
158 while i < 256 { raw[i] = 0; i = i + 1 }
159
160 // LFH at offset 0: sig, version 20, flag 0, method 0 (stored),
161 // time/date 0, crc 0, csize 5, usize 5, name_len 3 "foo",
162 // extra_len 0.
163 raw[0] = 0x50; raw[1] = 0x4b; raw[2] = 0x03; raw[3] = 0x04
164 raw[4] = 20; raw[5] = 0
165 raw[6] = 0; raw[7] = 0
166 raw[8] = 0; raw[9] = 0
167 raw[18] = 5 // compressed_size low byte
168 raw[22] = 5 // uncompressed_size low byte
169 raw[26] = 3 // name_len
170 raw[30] = 0x66 // 'f'
171 raw[31] = 0x6F // 'o'
172 raw[32] = 0x6F // 'o'
173
174 // EOCD at offset 40: sig, zeros, entries 1, cd_size 0, cd_off 38,
175 // comment_len 0. 22 bytes total.
176 let eocd: i64 = 40
177 raw[eocd] = 0x50; raw[eocd+1] = 0x4b
178 raw[eocd+2] = 0x05; raw[eocd+3] = 0x06
179 raw[eocd+8] = 1 // entries_this_disk low
180 raw[eocd+10] = 1 // entries_total low
181 raw[eocd+16] = 33 // cd_offset low (just any value)
182
183 let lfh_raw: *u8 = sys_mmap(128)
184 let lfh: *ZipLocalHeader = lfh_raw as *ZipLocalHeader
185 if zip_parse_lfh(raw, 256, 0, lfh) != 0 { return 1 }
186 if lfh.method != 0 { return 2 }
187 if lfh.name_len != 3 { return 3 }
188 if lfh.compressed_size != 5 { return 4 }
189 if lfh.header_len != 33 { return 5 }
190
191 let eocd_off: i64 = zip_find_eocd(raw, eocd + 22)
192 if eocd_off != eocd { return 6 }
193
194 let ee_raw: *u8 = sys_mmap(128)
195 let ee: *ZipEndOfCentralDir = ee_raw as *ZipEndOfCentralDir
196 if zip_parse_eocd(raw, 256, eocd_off, ee) != 0 { return 7 }
197 if ee.entries_total != 1 { return 8 }
198 if ee.cd_offset != 33 { return 9 }
199
200 if zip_method_known(8) != 1 { return 10 }
201 if zip_method_known(999) != 0 { return 11 }
202 return 0
203}