nx_zip.nx source
↩ module page · 264 lines · 9601 B
1// nx_zip.nx -- ZIP archive writer + reader (STORED method): a COMPLETE pair.
2//
3// The tree could READ zips (nx_zip_read.nx) and could not write one. That is
4// half a format: EPUB, OOXML, JAR and every download bundle are ZIP, and
5// producing them was impossible. This closes it with a writer and an
6// independent reader in one file, so it proves by exact round-trip.
7//
8// THE CRC IS THE REFLECTED ONE. ZIP uses CRC-32 with polynomial 0xEDB88320
9// (the reflected form of 0x04C11DB7), init and final-xor both 0xFFFFFFFF.
10// This is a DIFFERENT algorithm from the one nx_ogg.nx uses, which is the
11// same polynomial UNreflected with zero init. Two formats in this tree now
12// use "CRC-32" and they are not interchangeable -- swapping them yields
13// archives that every unzip rejects with a checksum error. T1 pins this one
14// to the classic check value: CRC32("123456789") == 0xCBF43926.
15//
16// THE OFFSET IS WRITTEN TWICE. Each entry's metadata appears in BOTH the
17// local header and the central directory, and the central directory also
18// carries the local header's byte offset. Readers trust the CENTRAL
19// DIRECTORY -- an archive whose local headers are fine but whose central
20// offsets are wrong opens as empty or corrupt, and the local headers give no
21// hint anything is off.
22//
23// STORED, NOT DEFLATED. Method 0 stores bytes verbatim. That is a fully
24// valid ZIP that every tool opens; wiring nx_deflate_enc.nx in as method 8 is
25// a compression improvement, not a correctness one.
26//
27// genealogy_id: pkware_appnote_zip
28// lineage_id: nx_zip_v1
29// license_tier: ORIGINAL
30
31import "nx_syscalls.nx"
32const NX_MAGIC_65535: i64 = 65535
33
34const NX_ZIP_SIG_LOCAL: i64 = 0x04034b50
35const NX_ZIP_SIG_CD: i64 = 0x02014b50
36const NX_ZIP_SIG_EOCD: i64 = 0x06054b50
37const NX_ZIP_LOCAL_LEN: i64 = 30
38const NX_ZIP_CD_LEN: i64 = 46
39const NX_ZIP_EOCD_LEN: i64 = 22
40const NX_ZIP_METHOD_STORE: i64 = 0
41const NX_ZIP_VERSION: i64 = 20
42const NX_ZIP_CRC_POLY: i64 = 0xedb88320
43const NX_ZIP_U32: i64 = 0xffffffff
44
45// writer state slots
46const NX_ZIP_ST_OFF: i64 = 0
47const NX_ZIP_ST_CDOFF: i64 = 1
48const NX_ZIP_ST_COUNT: i64 = 2
49
50// reader field slots
51const NX_ZIP_FLD_CRC: i64 = 0
52const NX_ZIP_FLD_CSIZE: i64 = 1
53const NX_ZIP_FLD_USIZE: i64 = 2
54const NX_ZIP_FLD_NAMELEN: i64 = 3
55const NX_ZIP_FLD_NAMEOFF: i64 = 4
56const NX_ZIP_FLD_LOCALOFF: i64 = 5
57const NX_ZIP_FLD_NEXTCD: i64 = 6
58const NX_ZIP_FLD_METHOD: i64 = 7
59
60// ===== the REFLECTED CRC-32 =======================================
61
62func nxzip_crc32(data: *u8, n: i64) -> i64 {
63 var crc: i64 = NX_ZIP_U32
64 var i: i64 = 0
65 while i < n {
66 crc = crc ^ ((data[i] as i64) & 255)
67 var b: i64 = 0
68 while b < 8 {
69 if (crc & 1) != 0 {
70 crc = ((crc >> 1) ^ NX_ZIP_CRC_POLY) & NX_ZIP_U32
71 } else {
72 crc = (crc >> 1) & NX_ZIP_U32
73 }
74 b = b + 1
75 }
76 i = i + 1
77 }
78 return (crc ^ NX_ZIP_U32) & NX_ZIP_U32
79}
80
81// ===== little-endian fields =======================================
82
83func nxzip_w16(o: *u8, p: i64, v: i64) -> i64 {
84 o[p] = (v & 255) as u8
85 o[p+1] = ((v >> 8) & 255) as u8
86 return 2
87}
88
89func nxzip_w32(o: *u8, p: i64, v: i64) -> i64 {
90 o[p] = (v & 255) as u8
91 o[p+1] = ((v >> 8) & 255) as u8
92 o[p+2] = ((v >> 16) & 255) as u8
93 o[p+3] = ((v >> 24) & 255) as u8
94 return 4
95}
96
97func nxzip_r16(b: *u8, p: i64) -> i64 {
98 return ((b[p] as i64) & 255) | (((b[p+1] as i64) & 255) << 8)
99}
100
101func nxzip_r32(b: *u8, p: i64) -> i64 {
102 return ((b[p] as i64) & 255) | (((b[p+1] as i64) & 255) << 8)
103 | (((b[p+2] as i64) & 255) << 16) | (((b[p+3] as i64) & 255) << 24)
104}
105
106func nxzip_strlen(s: *u8) -> i64 {
107 var i: i64 = 0
108 while s[i] != (0 as u8) { i = i + 1 }
109 return i
110}
111
112// ===== add one stored entry =======================================
113//
114// Writes the local header + data into `out`, and the matching central
115// directory record into `cd`. st carries the running offsets and count.
116// Returns 1, or 0 if either buffer cannot hold the entry -- never a partial
117// entry, which would checksum fine and desync the central directory.
118
119func nxzip_add_stored(out: *u8, cap: i64, cd: *u8, cdcap: i64, st: *i64,
120 name: *u8, data: *u8, len: i64) -> i64 {
121 if len < 0 { return 0 }
122 let nlen: i64 = nxzip_strlen(name)
123 if nlen <= 0 { return 0 }
124 if nlen > NX_MAGIC_65535 { return 0 }
125 let off: i64 = st[NX_ZIP_ST_OFF]
126 let cdoff: i64 = st[NX_ZIP_ST_CDOFF]
127 if off + NX_ZIP_LOCAL_LEN + nlen + len > cap { return 0 }
128 if cdoff + NX_ZIP_CD_LEN + nlen > cdcap { return 0 }
129
130 let crc: i64 = nxzip_crc32(data, len)
131
132 nxzip_w32(out, off, NX_ZIP_SIG_LOCAL)
133 nxzip_w16(out, off + 4, NX_ZIP_VERSION)
134 nxzip_w16(out, off + 6, 0)
135 nxzip_w16(out, off + 8, NX_ZIP_METHOD_STORE)
136 nxzip_w16(out, off + 10, 0)
137 nxzip_w16(out, off + 12, 0)
138 nxzip_w32(out, off + 14, crc)
139 nxzip_w32(out, off + 18, len)
140 nxzip_w32(out, off + 22, len)
141 nxzip_w16(out, off + 26, nlen)
142 nxzip_w16(out, off + 28, 0)
143 var i: i64 = 0
144 while i < nlen { out[off + NX_ZIP_LOCAL_LEN + i] = name[i]; i = i + 1 }
145 let dstart: i64 = off + NX_ZIP_LOCAL_LEN + nlen
146 i = 0
147 while i < len { out[dstart + i] = data[i]; i = i + 1 }
148
149 nxzip_w32(cd, cdoff, NX_ZIP_SIG_CD)
150 nxzip_w16(cd, cdoff + 4, NX_ZIP_VERSION)
151 nxzip_w16(cd, cdoff + 6, NX_ZIP_VERSION)
152 nxzip_w16(cd, cdoff + 8, 0)
153 nxzip_w16(cd, cdoff + 10, NX_ZIP_METHOD_STORE)
154 nxzip_w16(cd, cdoff + 12, 0)
155 nxzip_w16(cd, cdoff + 14, 0)
156 nxzip_w32(cd, cdoff + 16, crc)
157 nxzip_w32(cd, cdoff + 20, len)
158 nxzip_w32(cd, cdoff + 24, len)
159 nxzip_w16(cd, cdoff + 28, nlen)
160 nxzip_w16(cd, cdoff + 30, 0)
161 nxzip_w16(cd, cdoff + 32, 0)
162 nxzip_w16(cd, cdoff + 34, 0)
163 nxzip_w16(cd, cdoff + 36, 0)
164 nxzip_w32(cd, cdoff + 38, 0)
165 // the offset readers actually trust
166 nxzip_w32(cd, cdoff + 42, off)
167 i = 0
168 while i < nlen { cd[cdoff + NX_ZIP_CD_LEN + i] = name[i]; i = i + 1 }
169
170 st[NX_ZIP_ST_OFF] = dstart + len
171 st[NX_ZIP_ST_CDOFF] = cdoff + NX_ZIP_CD_LEN + nlen
172 st[NX_ZIP_ST_COUNT] = st[NX_ZIP_ST_COUNT] + 1
173 return 1
174}
175
176// ===== finalise ===================================================
177//
178// Appends the accumulated central directory then the end-of-central-directory
179// record. Returns the total archive length, or 0.
180
181func nxzip_finalize(out: *u8, cap: i64, cd: *u8, st: *i64) -> i64 {
182 let off: i64 = st[NX_ZIP_ST_OFF]
183 let cdlen: i64 = st[NX_ZIP_ST_CDOFF]
184 let count: i64 = st[NX_ZIP_ST_COUNT]
185 if off + cdlen + NX_ZIP_EOCD_LEN > cap { return 0 }
186
187 var i: i64 = 0
188 while i < cdlen { out[off + i] = cd[i]; i = i + 1 }
189 let e: i64 = off + cdlen
190 nxzip_w32(out, e, NX_ZIP_SIG_EOCD)
191 nxzip_w16(out, e + 4, 0)
192 nxzip_w16(out, e + 6, 0)
193 nxzip_w16(out, e + 8, count)
194 nxzip_w16(out, e + 10, count)
195 nxzip_w32(out, e + 12, cdlen)
196 nxzip_w32(out, e + 16, off)
197 nxzip_w16(out, e + 20, 0)
198 return e + NX_ZIP_EOCD_LEN
199}
200
201// ===== reader =====================================================
202//
203// Scans backwards for the EOCD signature, which is how every real unzip finds
204// it (a trailing comment may follow the record).
205
206func nxzip_find_eocd(data: *u8, n: i64) -> i64 {
207 if n < NX_ZIP_EOCD_LEN { return 0 - 1 }
208 var p: i64 = n - NX_ZIP_EOCD_LEN
209 while p >= 0 {
210 if nxzip_r32(data, p) == NX_ZIP_SIG_EOCD { return p }
211 p = p - 1
212 }
213 return 0 - 1
214}
215
216func nxzip_count(data: *u8, n: i64) -> i64 {
217 let e: i64 = nxzip_find_eocd(data, n)
218 if e < 0 { return 0 - 1 }
219 return nxzip_r16(data, e + 10)
220}
221
222func nxzip_cd_start(data: *u8, n: i64) -> i64 {
223 let e: i64 = nxzip_find_eocd(data, n)
224 if e < 0 { return 0 - 1 }
225 return nxzip_r32(data, e + 16)
226}
227
228// Read one central-directory record at cdp. Returns 1 and fills fld, or 0.
229func nxzip_read_cd(data: *u8, n: i64, cdp: i64, fld: *i64) -> i64 {
230 if cdp < 0 { return 0 }
231 if cdp + NX_ZIP_CD_LEN > n { return 0 }
232 if nxzip_r32(data, cdp) != NX_ZIP_SIG_CD { return 0 }
233 let nlen: i64 = nxzip_r16(data, cdp + 28)
234 let elen: i64 = nxzip_r16(data, cdp + 30)
235 let clen: i64 = nxzip_r16(data, cdp + 32)
236 if cdp + NX_ZIP_CD_LEN + nlen + elen + clen > n { return 0 }
237 fld[NX_ZIP_FLD_METHOD] = nxzip_r16(data, cdp + 10)
238 fld[NX_ZIP_FLD_CRC] = nxzip_r32(data, cdp + 16)
239 fld[NX_ZIP_FLD_CSIZE] = nxzip_r32(data, cdp + 20)
240 fld[NX_ZIP_FLD_USIZE] = nxzip_r32(data, cdp + 24)
241 fld[NX_ZIP_FLD_NAMELEN] = nlen
242 fld[NX_ZIP_FLD_NAMEOFF] = cdp + NX_ZIP_CD_LEN
243 fld[NX_ZIP_FLD_LOCALOFF] = nxzip_r32(data, cdp + 42)
244 fld[NX_ZIP_FLD_NEXTCD] = cdp + NX_ZIP_CD_LEN + nlen + elen + clen
245 return 1
246}
247
248// Resolve an entry's data offset from its LOCAL header and verify the CRC.
249// Returns the data offset, or -1 if the local header is wrong or the stored
250// bytes do not match the recorded checksum.
251func nxzip_entry_data(data: *u8, n: i64, fld: *i64) -> i64 {
252 let lo: i64 = fld[NX_ZIP_FLD_LOCALOFF]
253 if lo < 0 { return 0 - 1 }
254 if lo + NX_ZIP_LOCAL_LEN > n { return 0 - 1 }
255 if nxzip_r32(data, lo) != NX_ZIP_SIG_LOCAL { return 0 - 1 }
256 if nxzip_r16(data, lo + 8) != NX_ZIP_METHOD_STORE { return 0 - 1 }
257 let nlen: i64 = nxzip_r16(data, lo + 26)
258 let elen: i64 = nxzip_r16(data, lo + 28)
259 let doff: i64 = lo + NX_ZIP_LOCAL_LEN + nlen + elen
260 let sz: i64 = fld[NX_ZIP_FLD_CSIZE]
261 if doff + sz > n { return 0 - 1 }
262 if nxzip_crc32(data + doff, sz) != fld[NX_ZIP_FLD_CRC] { return 0 - 1 }
263 return doff
264}