nx_zip_header.nx source
↩ module page · 210 lines · 7707 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
29// nx_safety_envelope:
30// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
31// sil_target: SIL1
32// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
33// verdict: NOT_YET_EVALUATED
34
35import "nx_syscalls.nx"
36const ZIP_MAGIC_66000: i64 = 66000
37
38const ZIP_LFH_SIG: i64 = 0x04034b50
39const ZIP_EOCD_SIG: i64 = 0x06054b50
40
41const ZIP_ERR_FORMAT: i64 = -1
42const ZIP_ERR_SHORT: i64 = -2
43
44const ZIP64_MARKER: i64 = 0xFFFFFFFF
45
46struct ZipLocalHeader {
47 version_needed: i64,
48 flag: i64,
49 method: i64, // 0 stored, 8 deflate, 14 lzma, 93 zstd
50 mod_time: i64, // MS-DOS time
51 mod_date: i64, // MS-DOS date
52 crc32: i64,
53 compressed_size: i64,
54 uncompressed_size: i64,
55 name_off: i64, name_len: i64,
56 extra_off: i64, extra_len: i64,
57 // Header length so caller knows where the compressed payload starts.
58 header_len: i64,
59}
60
61struct ZipEndOfCentralDir {
62 disk_number: i64,
63 cd_start_disk: i64,
64 entries_this_disk: i64,
65 entries_total: i64,
66 cd_size: i64,
67 cd_offset: i64,
68 comment_off: i64,
69 comment_len: i64,
70}
71
72// Little-endian reads.
73func zip_read_u16(buf: *u8, off: i64) -> i64 {
74 return buf[off] | (buf[off + 1] << 8)
75}
76
77func zip_read_u32(buf: *u8, off: i64) -> i64 {
78 let b0: i64 = buf[off]
79 let b1: i64 = buf[off + 1]
80 let b2: i64 = buf[off + 2]
81 let b3: i64 = buf[off + 3]
82 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
83}
84
85// Parse a Local File Header at buf[off]. Returns 0 on success
86// or a negative ZIP_ERR_*.
87func zip_parse_lfh(buf: *u8, n: i64, off: i64, h: *ZipLocalHeader) -> i64 {
88 if n < off + 30 { return ZIP_ERR_SHORT }
89 if zip_read_u32(buf, off) != ZIP_LFH_SIG { return ZIP_ERR_FORMAT }
90
91 h.version_needed = zip_read_u16(buf, off + 4)
92 h.flag = zip_read_u16(buf, off + 6)
93 h.method = zip_read_u16(buf, off + 8)
94 h.mod_time = zip_read_u16(buf, off + 10)
95 h.mod_date = zip_read_u16(buf, off + 12)
96 h.crc32 = zip_read_u32(buf, off + 14)
97 h.compressed_size = zip_read_u32(buf, off + 18)
98 h.uncompressed_size = zip_read_u32(buf, off + 22)
99 let name_len: i64 = zip_read_u16(buf, off + 26)
100 let extra_len: i64 = zip_read_u16(buf, off + 28)
101
102 let name_off: i64 = off + 30
103 if n < name_off + name_len + extra_len { return ZIP_ERR_SHORT }
104 h.name_off = name_off
105 h.name_len = name_len
106 h.extra_off = name_off + name_len
107 h.extra_len = extra_len
108 h.header_len = 30 + name_len + extra_len
109 return 0
110}
111
112// Scan backwards from end-of-buffer for the EOCD signature. ZIP
113// allows a trailing comment up to 65535 bytes, so we bound the
114// scan to the last 66000 bytes. Returns offset of EOCD or -1.
115func zip_find_eocd(buf: *u8, n: i64) -> i64 {
116 var limit: i64 = n - ZIP_MAGIC_66000
117 if limit < 0 { limit = 0 }
118 // EOCD is at least 22 bytes; last possible start = n-22.
119 var off: i64 = n - 22
120 while off >= limit {
121 if off + 3 < n {
122 if zip_read_u32(buf, off) == ZIP_EOCD_SIG { return off }
123 }
124 off = off - 1
125 }
126 return -1
127}
128
129// Parse EOCD at given offset.
130func zip_parse_eocd(buf: *u8, n: i64, off: i64,
131 e: *ZipEndOfCentralDir) -> i64 {
132 if n < off + 22 { return ZIP_ERR_SHORT }
133 if zip_read_u32(buf, off) != ZIP_EOCD_SIG { return ZIP_ERR_FORMAT }
134
135 e.disk_number = zip_read_u16(buf, off + 4)
136 e.cd_start_disk = zip_read_u16(buf, off + 6)
137 e.entries_this_disk = zip_read_u16(buf, off + 8)
138 e.entries_total = zip_read_u16(buf, off + 10)
139 e.cd_size = zip_read_u32(buf, off + 12)
140 e.cd_offset = zip_read_u32(buf, off + 16)
141 let comment_len: i64 = zip_read_u16(buf, off + 20)
142 if n < off + 22 + comment_len { return ZIP_ERR_SHORT }
143 e.comment_off = off + 22
144 e.comment_len = comment_len
145 return 0
146}
147
148// Helper: map compression method code to a short name length.
149// Returns 0 if unknown. Used for debug printing; callers print
150// from a static table.
151func zip_method_known(method: i64) -> i64 {
152 if method == 0 { return 1 } // stored
153 if method == 8 { return 1 } // deflate
154 if method == 14 { return 1 } // lzma
155 if method == 93 { return 1 } // zstd
156 if method == 95 { return 1 } // xz
157 if method == 98 { return 1 } // ppmd
158 return 0
159}
160
161// Compile-only smoke: forge minimal LFH + EOCD in a buffer.
162func main() -> i64 {
163 let raw: *u8 = sys_mmap(256)
164 var i: i64 = 0
165 while i < 256 { raw[i] = 0; i = i + 1 }
166
167 // LFH at offset 0: sig, version 20, flag 0, method 0 (stored),
168 // time/date 0, crc 0, csize 5, usize 5, name_len 3 "foo",
169 // extra_len 0.
170 raw[0] = 0x50; raw[1] = 0x4b; raw[2] = 0x03; raw[3] = 0x04
171 raw[4] = 20; raw[5] = 0
172 raw[6] = 0; raw[7] = 0
173 raw[8] = 0; raw[9] = 0
174 raw[18] = 5 // compressed_size low byte
175 raw[22] = 5 // uncompressed_size low byte
176 raw[26] = 3 // name_len
177 raw[30] = 0x66 // 'f'
178 raw[31] = 0x6F // 'o'
179 raw[32] = 0x6F // 'o'
180
181 // EOCD at offset 40: sig, zeros, entries 1, cd_size 0, cd_off 38,
182 // comment_len 0. 22 bytes total.
183 let eocd: i64 = 40
184 raw[eocd] = 0x50; raw[eocd+1] = 0x4b
185 raw[eocd+2] = 0x05; raw[eocd+3] = 0x06
186 raw[eocd+8] = 1 // entries_this_disk low
187 raw[eocd+10] = 1 // entries_total low
188 raw[eocd+16] = 33 // cd_offset low (just any value)
189
190 let lfh_raw: *u8 = sys_mmap(128)
191 let lfh: *ZipLocalHeader = lfh_raw as *ZipLocalHeader
192 if zip_parse_lfh(raw, 256, 0, lfh) != 0 { return 1 }
193 if lfh.method != 0 { return 2 }
194 if lfh.name_len != 3 { return 3 }
195 if lfh.compressed_size != 5 { return 4 }
196 if lfh.header_len != 33 { return 5 }
197
198 let eocd_off: i64 = zip_find_eocd(raw, eocd + 22)
199 if eocd_off != eocd { return 6 }
200
201 let ee_raw: *u8 = sys_mmap(128)
202 let ee: *ZipEndOfCentralDir = ee_raw as *ZipEndOfCentralDir
203 if zip_parse_eocd(raw, 256, eocd_off, ee) != 0 { return 7 }
204 if ee.entries_total != 1 { return 8 }
205 if ee.cd_offset != 33 { return 9 }
206
207 if zip_method_known(8) != 1 { return 10 }
208 if zip_method_known(999) != 0 { return 11 }
209 return 0
210}