archive.bend
bend-kit-archive@0.5.0.0 · 374 lines · 0 definitions
1# ZIP archives read over Bytes: stored and DEFLATE entries, checked against their CRC-32 and sizes. Source: https://github.com/paymog/bend-kit/tree/main/archive
2import Base
3import bend-kit-bytes@0.3.3.0/bytes.bend as Bytes
4import bend-kit-zlib@0.4.0.0/zlib.bend as Zlib
5import bend-kit-hash@0.3.0.0/hash.bend as Hash
6
7# One member: its name and contents as raw bytes, its method (0 stored, 8 DEFLATE), and its CRC-32.
8type Entry is Type:
9 Entry{name: Bytes.Bytes, data: Bytes.Bytes, method: U32, crc: U32}
10
11# A checked central directory entry. name and data are byte offsets into the input;
12# packed is the stored or compressed length, size the length after decoding.
13type Header is Data:
14 Header{name: U32, name_len: U32, method: U32, crc: U32, packed: U32, size: U32, data: U32}
15
16# The methods read: 0 (stored) and 8 (DEFLATE).
17def supported(+method: U32) -> Bool:
18 Bool.or(U32.is_eq(method, 0), U32.is_eq(method, 8))
19
20def crc.bytes(b: Bytes.Bytes) -> Bytes.Bytes & U32:
21 Hash.crc32(b)
22
23def crc.value(r: Bytes.Bytes & U32) -> U32:
24 (b, crc) = r
25 crc
26
27def crc32(b: Bytes.Bytes) -> U32:
28 crc.value(crc.bytes(b))
29
30# n bytes from i as a list. Callers keep i + n within the buffer.
31def grab.go(k: Nat, r: Array<U32> & U32, +j: U32, acc: List<&2, U32>) -> Array<U32> & List<&2, U32>:
32 match k:
33 case 0n:
34 (a, v) = r
35 (a, acc)
36 case 1n+p:
37 (a, +v) = r
38 grab.go(p, Bytes.peek(a, (j - 1 : U32)), (j - 1 : U32), Con{v, acc})
39
40def grab(a: Array<U32>, +i: U32, +n: U32) -> Array<U32> & List<&2, U32>:
41 +j = (i + n - 1 : U32)
42 grab.go(U32.to_nat(n), Bytes.peek(a, j), j, Nil{})
43
44def at(xs: List<&2, U32>, +i: U32) -> U32:
45 match xs:
46 case Nil{}:
47 0
48 case Con{h, t}:
49 Bool.pick(U32, U32.is_zero(i), h, at(t, (i - 1 : U32)))
50
51# Little-endian fields of a grabbed record.
52def u16(+xs: List<&2, U32>, +o: U32) -> U32:
53 (at(xs, o) .|. (at(xs, (o + 1 : U32)) << 8n) : U32)
54
55def u32(+xs: List<&2, U32>, +o: U32) -> U32:
56 (u16(xs, o) .|. (u16(xs, (o + 2 : U32)) << 16n) : U32)
57
58# The first failed check's message, outermost first.
59def why(+ok: Bool, +msg: String, +rest: Maybe<&2, String>) -> Maybe<&2, String>:
60 Bool.pick(Maybe<&2, String>, ok, rest, Some{msg})
61
62# General purpose flags: encrypted (bit 0), strong encryption (bit 6), masked headers (bit 13).
63def encrypted(+flags: U32) -> Bool:
64 Bool.not(U32.is_zero((flags .&. 8257 : U32)))
65
66# Do n bytes at x and at y match? r holds the previous byte's answer.
67def same.at3(r: Array<U32> & U32, +u: U32) -> Array<U32> & Bool:
68 (a, +v) = r
69 (a, U32.is_eq(u, v))
70
71def same.at2(r: Array<U32> & U32, +y: U32) -> Array<U32> & Bool:
72 (a, +u) = r
73 same.at3(Bytes.peek(a, y), u)
74
75def same.go(n: Nat, r: Array<U32> & Bool, +x: U32, +y: U32) -> Array<U32> & Bool:
76 match n:
77 case 0n:
78 r
79 case 1n+q:
80 (a, ok) = r
81 match ok:
82 case False{}:
83 (a, False{})
84 case True{}:
85 same.go(q, same.at2(Bytes.peek(a, x), y), (x + 1 : U32), (y + 1 : U32))
86
87# End of central directory (APPNOTE 4.3.16): the last signature whose comment length reaches the end exactly.
88def eocd.of(r: Array<U32> & List<&2, U32>, +len: U32, +p: U32) -> Array<U32> & Bool:
89 (a, +x) = r
90 (a, Bool.and(U32.is_eq(u32(x, 0), 101010256), U32.is_eq(u16(x, 20), (len - p - 22 : U32))))
91
92def eocd.win(a: Array<U32>, +len: U32, +p: U32) -> Array<U32> & Bool:
93 eocd.of(grab(a, p, 22), len, p)
94
95# f counts the positions left below p; a comment is at most 65535 bytes.
96def eocd.go(f: Nat, r: Array<U32> & Bool, +len: U32, +p: U32) -> Array<U32> & Maybe<&2, U32>:
97 match f:
98 case 0n:
99 (a, hit) = r
100 (a, Bool.pick(Maybe<&2, U32>, hit, Some{p}, None{}))
101 case 1n+q:
102 (a, hit) = r
103 match hit:
104 case True{}:
105 (a, Some{p})
106 case False{}:
107 eocd.go(q, eocd.win(a, len, (p - 1 : U32)), len, (p - 1 : U32))
108
109def cd.fin.end(ok: Bool, acc: List<&2, Header>) -> Result<&1, &1, U32 & String, List<&2, Header>>:
110 match ok:
111 case True{}:
112 Done{List.reverse(&2, Header, acc)}
113 case False{}:
114 Fail{(22, "central directory size does not match its entries")}
115
116def cd.fin(res: Result<&1, &1, U32 & String, U32 & List<&2, Header>>, +end: U32) -> Result<&1, &1, U32 & String, List<&2, Header>>:
117 match res:
118 case Fail{e}:
119 Fail{e}
120 case Done{(+p, acc)}:
121 cd.fin.end(U32.is_eq(p, end), acc)
122
123def lh.push(ok: Bool, a: Array<U32>, +hd: Header, +next: U32, acc: List<&2, Header>) -> Array<U32> & Result<&1, &1, U32 & String, U32 & List<&2, Header>>:
124 match ok:
125 case False{}:
126 (a, Fail{(22, "local header name differs from the central directory")})
127 case True{}:
128 (a, Done{(next, Con{hd, acc})})
129
130def lh.same(r: Array<U32> & Bool, +hd: Header, +next: U32, acc: List<&2, Header>) -> Array<U32> & Result<&1, &1, U32 & String, U32 & List<&2, Header>>:
131 (a, ok) = r
132 lh.push(ok, a, hd, next, acc)
133
134def lh.name(err: Maybe<&2, String>, a: Array<U32>, +lname: U32, +hd: Header, +next: U32, acc: List<&2, Header>) -> Array<U32> & Result<&1, &1, U32 & String, U32 & List<&2, Header>>:
135 match err:
136 case Some{m}:
137 (a, Fail{(22, m)})
138 case None{}:
139 Header{+name, +nlen, method, crc, packed, size, data} = hd
140 lh.same(same.go(U32.to_nat(nlen), (a, True{}), name, lname), hd, next, acc)
141
142# Local file header (APPNOTE 4.3.7): it must agree with the central directory, and its data must end before the directory.
143def lh.err(+l: List<&2, U32>, +method: U32, +nlen: U32, +packed: U32, +cdoff: U32, +lname: U32, +extra: U32) -> Maybe<&2, String>:
144 +data = (lname + extra : U32)
145 why(U32.is_eq(u32(l, 0), 67324752), "bad local header signature",
146 why(Bool.not(encrypted(u16(l, 6))), "encrypted entries are unsupported",
147 why(U32.is_eq(u16(l, 8), method), "local header method differs from the central directory",
148 why(U32.is_eq(u16(l, 26), nlen), "local header name differs from the central directory",
149 why(Bytes.fits(cdoff, lname, extra), "local header runs past the central directory",
150 why(Bytes.fits(cdoff, data, packed), "entry data runs past the central directory", None{}))))))
151
152def lh.head(r: Array<U32> & List<&2, U32>, +h: List<&2, U32>, +p: U32, +next: U32, +cdoff: U32, acc: List<&2, Header>) -> Array<U32> & Result<&1, &1, U32 & String, U32 & List<&2, Header>>:
153 (a, +l) = r
154 +nlen = u16(h, 28)
155 +method = u16(h, 10)
156 +packed = u32(h, 20)
157 +lname = (u32(h, 42) + 30 : U32)
158 +extra = (u16(l, 26) + u16(l, 28) : U32)
159 lh.name(lh.err(l, method, nlen, packed, cdoff, lname, extra), a, lname, Header{(p + 46 : U32), nlen, method, u32(h, 16), packed, u32(h, 24), (lname + extra : U32)}, next, acc)
160
161def lh.at(ok: Bool, a: Array<U32>, +h: List<&2, U32>, +p: U32, +next: U32, +cdoff: U32, acc: List<&2, Header>) -> Array<U32> & Result<&1, &1, U32 & String, U32 & List<&2, Header>>:
162 match ok:
163 case False{}:
164 (a, Fail{(22, "local header is out of bounds")})
165 case True{}:
166 lh.head(grab(a, u32(h, 42), 30), h, p, next, cdoff, acc)
167
168# Central directory file header (APPNOTE 4.3.12) at p, with the directory ending at end.
169def cd.err(+h: List<&2, U32>, +p: U32, +end: U32) -> Maybe<&2, String>:
170 +method = u16(h, 10)
171 +packed = u32(h, 20)
172 +size = u32(h, 24)
173 +rest = (u16(h, 28) + u16(h, 30) + u16(h, 32) : U32)
174 +zip64 = Bool.or(Bool.or(U32.is_eq(packed, 4294967295), U32.is_eq(size, 4294967295)), U32.is_eq(u32(h, 42), 4294967295))
175 why(U32.is_eq(u32(h, 0), 33639248), "bad central directory header signature",
176 why(Bool.not(encrypted(u16(h, 8))), "encrypted entries are unsupported",
177 why(supported(method), "unsupported compression method " ++ U32.show(method),
178 why(Bool.not(zip64), "ZIP64 is unsupported",
179 why(U32.is_eq(u16(h, 34), 0), "multi-disk ZIP is unsupported",
180 why(Bytes.fits(end, (p + 46 : U32), rest), "central directory header runs past the directory",
181 why(Bool.or(U32.is_eq(method, 8), U32.is_eq(packed, size)), "stored entry sizes differ", None{})))))))
182
183def cd.head.of(err: Maybe<&2, String>, a: Array<U32>, +h: List<&2, U32>, +p: U32, +cdoff: U32, acc: List<&2, Header>) -> Array<U32> & Result<&1, &1, U32 & String, U32 & List<&2, Header>>:
184 match err:
185 case Some{m}:
186 (a, Fail{(22, m)})
187 case None{}:
188 +next = (p + 46 + u16(h, 28) + u16(h, 30) + u16(h, 32) : U32)
189 lh.at(Bytes.fits(cdoff, u32(h, 42), 30), a, h, p, next, cdoff, acc)
190
191def cd.head(r: Array<U32> & List<&2, U32>, +p: U32, +end: U32, +cdoff: U32, acc: List<&2, Header>) -> Array<U32> & Result<&1, &1, U32 & String, U32 & List<&2, Header>>:
192 (a, +h) = r
193 cd.head.of(cd.err(h, p, end), a, h, p, cdoff, acc)
194
195def cd.entry(ok: Bool, a: Array<U32>, +p: U32, +end: U32, +cdoff: U32, acc: List<&2, Header>) -> Array<U32> & Result<&1, &1, U32 & String, U32 & List<&2, Header>>:
196 match ok:
197 case False{}:
198 (a, Fail{(22, "central directory is truncated")})
199 case True{}:
200 cd.head(grab(a, p, 46), p, end, cdoff, acc)
201
202# k entries left; r holds the next header's offset and the headers so far, reversed.
203def cd.go(k: Nat, r: Array<U32> & Result<&1, &1, U32 & String, U32 & List<&2, Header>>, +end: U32, +cdoff: U32) -> Array<U32> & Result<&1, &1, U32 & String, List<&2, Header>>:
204 match k:
205 case 0n:
206 (a, res) = r
207 (a, cd.fin(res, end))
208 case 1n+q:
209 (a, res) = r
210 match res:
211 case Fail{e}:
212 (a, Fail{e})
213 case Done{(+p, acc)}:
214 cd.go(q, cd.entry(Bytes.fits(end, p, 46), a, p, end, cdoff, acc), end, cdoff)
215
216def eocd.err(+x: List<&2, U32>, +e: U32) -> Maybe<&2, String>:
217 +zip64 = Bool.or(Bool.or(U32.is_eq(u16(x, 10), 65535), U32.is_eq(u32(x, 12), 4294967295)), U32.is_eq(u32(x, 16), 4294967295))
218 why(Bool.and(Bool.and(U32.is_eq(u16(x, 4), 0), U32.is_eq(u16(x, 6), 0)), U32.is_eq(u16(x, 8), u16(x, 10))), "multi-disk ZIP is unsupported",
219 why(Bool.not(zip64), "ZIP64 is unsupported",
220 why(Bytes.fits(e, u32(x, 16), u32(x, 12)), "central directory is out of bounds", None{})))
221
222def eocd.checked(err: Maybe<&2, String>, a: Array<U32>, +x: List<&2, U32>) -> Array<U32> & Result<&1, &1, U32 & String, List<&2, Header>>:
223 match err:
224 case Some{m}:
225 (a, Fail{(22, m)})
226 case None{}:
227 +off = u32(x, 16)
228 cd.go(U32.to_nat(u16(x, 10)), (a, Done{(off, Nil{})}), (off + u32(x, 12) : U32), off)
229
230def eocd.parse(r: Array<U32> & List<&2, U32>, +e: U32) -> Array<U32> & Result<&1, &1, U32 & String, List<&2, Header>>:
231 (a, +x) = r
232 eocd.checked(eocd.err(x, e), a, x)
233
234def eocd.found(r: Array<U32> & Maybe<&2, U32>) -> Array<U32> & Result<&1, &1, U32 & String, List<&2, Header>>:
235 (a, m) = r
236 match m:
237 case None{}:
238 (a, Fail{(22, "no end of central directory record")})
239 case Some{+e}:
240 eocd.parse(grab(a, e, 22), e)
241
242def headers.at(short: Bool, a: Array<U32>, +len: U32) -> Array<U32> & Result<&1, &1, U32 & String, List<&2, Header>>:
243 match short:
244 case True{}:
245 (a, Fail{(22, "input is shorter than an end of central directory record")})
246 case False{}:
247 +p = (len - 22 : U32)
248 eocd.found(eocd.go(U32.to_nat(U32.min(p, 65535)), eocd.win(a, len, p), len, p))
249
250def headers.fin(+len: U32, r: Array<U32> & Result<&1, &1, U32 & String, List<&2, Header>>) -> Bytes.Bytes & Result<&1, &1, U32 & String, List<&2, Header>>:
251 (a, res) = r
252 (Bytes.Bytes{len, a}, res)
253
254# The input back, and its central directory with every bound, local header, and method checked. Pure: no data is decoded.
255def headers(input: Bytes.Bytes) -> Bytes.Bytes & Result<&1, &1, U32 & String, List<&2, Header>>:
256 Bytes.Bytes{+len, buf} = input
257 headers.fin(len, headers.at(U32.is_lt(len, 22), buf, len))
258
259def inflated(r: Result<&1, &1, U32 & String, U32 & Array<U32>>) -> Result<&1, &1, U32 & String, Bytes.Bytes>:
260 match r:
261 case Fail{e}:
262 (+code, msg) = e
263 Fail{Bool.pick(U32 & String, U32.is_eq(code, 27), (22, "entry size differs from the central directory"), (code, "DEFLATE entry: " ++ msg))}
264 case Done{(+n, w)}:
265 Done{Bytes.Bytes{n, w}}
266
267# Output past size is refused inside libz, so a bomb never grows beyond its declared size.
268def inflate.of(+size: U32, b: Bytes.Bytes) -> IO(Result<&1, &1, U32 & String, Bytes.Bytes>):
269 Bytes.Bytes{+n, buf} = b
270 do IO<Result<&1, &1, U32 & String, Bytes.Bytes>>:
271 r : Result<&1, &1, U32 & String, U32 & Array<U32>> <- Zlib.inflate.raw.words(size, n, buf)
272 return inflated(r)
273
274def decode(stored: Bool, +size: U32, raw: Bytes.Bytes) -> IO(Result<&1, &1, U32 & String, Bytes.Bytes>):
275 match stored:
276 case True{}:
277 IO.pure(Result<&1, &1, U32 & String, Bytes.Bytes>, Done{raw})
278 case False{}:
279 inflate.of(size, raw)
280
281def check.crc.of(ok: Bool, b: Bytes.Bytes) -> Result<&1, &1, U32 & String, Bytes.Bytes>:
282 match ok:
283 case True{}:
284 Done{b}
285 case False{}:
286 Fail{(22, "entry CRC-32 does not match")}
287
288def check.crc(r: Bytes.Bytes & U32, +crc: U32) -> Result<&1, &1, U32 & String, Bytes.Bytes>:
289 (b, +c) = r
290 check.crc.of(U32.is_eq(c, crc), b)
291
292def check.len.of(ok: Bool, b: Bytes.Bytes, +crc: U32) -> Result<&1, &1, U32 & String, Bytes.Bytes>:
293 match ok:
294 case False{}:
295 Fail{(22, "entry size differs from the central directory")}
296 case True{}:
297 check.crc(crc.bytes(b), crc)
298
299def check.len(r: Bytes.Bytes & U32, +size: U32, +crc: U32) -> Result<&1, &1, U32 & String, Bytes.Bytes>:
300 (b, +n) = r
301 check.len.of(U32.is_eq(n, size), b, crc)
302
303def check(+size: U32, +crc: U32, r: Result<&1, &1, U32 & String, Bytes.Bytes>) -> Result<&1, &1, U32 & String, Bytes.Bytes>:
304 match r:
305 case Fail{e}:
306 Fail{e}
307 case Done{b}:
308 check.len(Bytes.length(b), size, crc)
309
310def one.put(r: Result<&1, &1, U32 & String, Bytes.Bytes>, nm: Bytes.Bytes, +method: U32, +crc: U32, +left: U32, +size: U32, acc: List<&1, Entry>) -> Result<&1, &1, U32 & String, U32 & List<&1, Entry>>:
311 match r:
312 case Fail{e}:
313 Fail{e}
314 case Done{b}:
315 Done{((left - size : U32), Con{Entry{nm, b, method, crc}, acc})}
316
317def one.data(r: Bytes.Bytes & Bytes.Bytes, nm: Bytes.Bytes, +method: U32, +crc: U32, +size: U32, +left: U32, acc: List<&1, Entry>) -> IO(Bytes.Bytes & Result<&1, &1, U32 & String, U32 & List<&1, Entry>>):
318 (input, raw) = r
319 do IO<Bytes.Bytes & Result<&1, &1, U32 & String, U32 & List<&1, Entry>>>:
320 out : Result<&1, &1, U32 & String, Bytes.Bytes> <- decode(U32.is_eq(method, 0), size, raw)
321 return (input, one.put(check(size, crc, out), nm, method, crc, left, size, acc))
322
323def one.name(r: Bytes.Bytes & Bytes.Bytes, +method: U32, +crc: U32, +packed: U32, +size: U32, +data: U32, +left: U32, acc: List<&1, Entry>) -> IO(Bytes.Bytes & Result<&1, &1, U32 & String, U32 & List<&1, Entry>>):
324 (input, nm) = r
325 one.data(Bytes.slice(input, data, packed), nm, method, crc, size, left, acc)
326
327# left: the output still allowed under the caller's max.
328def one.cap(over: Bool, input: Bytes.Bytes, +hd: Header, +left: U32, acc: List<&1, Entry>) -> IO(Bytes.Bytes & Result<&1, &1, U32 & String, U32 & List<&1, Entry>>):
329 match over:
330 case True{}:
331 IO.pure(Bytes.Bytes & Result<&1, &1, U32 & String, U32 & List<&1, Entry>>, (input, Fail{(27, "archive output is larger than max")}))
332 case False{}:
333 Header{+name, +nlen, +method, +crc, +packed, +size, +data} = hd
334 one.name(Bytes.slice(input, name, nlen), method, crc, packed, size, data, left, acc)
335
336def one(+hd: Header, input: Bytes.Bytes, +left: U32, acc: List<&1, Entry>) -> IO(Bytes.Bytes & Result<&1, &1, U32 & String, U32 & List<&1, Entry>>):
337 Header{name, nlen, method, crc, packed, +size, data} = hd
338 one.cap(U32.is_lt(left, size), input, hd, left, acc)
339
340def run.fin(res: Result<&1, &1, U32 & String, U32 & List<&1, Entry>>) -> Result<&1, &1, U32 & String, List<&1, Entry>>:
341 match res:
342 case Fail{e}:
343 Fail{e}
344 case Done{(left, acc)}:
345 Done{List.reverse(&1, Entry, acc)}
346
347def run(xs: List<&2, Header>, st: Bytes.Bytes & Result<&1, &1, U32 & String, U32 & List<&1, Entry>>) -> IO(Result<&1, &1, U32 & String, List<&1, Entry>>):
348 match xs:
349 case Nil{}:
350 (input, res) = st
351 IO.pure(Result<&1, &1, U32 & String, List<&1, Entry>>, run.fin(res))
352 case Con{hd, t}:
353 (input, res) = st
354 match res:
355 case Fail{e}:
356 IO.pure(Result<&1, &1, U32 & String, List<&1, Entry>>, Fail{e})
357 case Done{(+left, acc)}:
358 do IO<Result<&1, &1, U32 & String, List<&1, Entry>>>:
359 next : Bytes.Bytes & Result<&1, &1, U32 & String, U32 & List<&1, Entry>> <- one(hd, input, left, acc)
360 run(t, next)
361
362def read.of(r: Bytes.Bytes & Result<&1, &1, U32 & String, List<&2, Header>>, +max: U32) -> IO(Result<&1, &1, U32 & String, List<&1, Entry>>):
363 (input, res) = r
364 match res:
365 case Fail{e}:
366 IO.pure(Result<&1, &1, U32 & String, List<&1, Entry>>, Fail{e})
367 case Done{xs}:
368 run(xs, (input, Done{(max, Nil{})}))
369
370# Every entry of a ZIP archive, in central directory order, or the first problem found.
371# Stored (0) and DEFLATE (8) entries only; no ZIP64, encryption, or multi-disk archives.
372# Output past max bytes in total fails with 27 (EFBIG). Malformed ZIP fails with 22 (EINVAL); libz load and allocation errors keep their host codes.
373def read(max: U32, input: Bytes.Bytes) -> IO(Result<&1, &1, U32 & String, List<&1, Entry>>):
374 read.of(headers(input), max)