Source file flux_unzip.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
type entry = {
filepath: string
; mtime: Ptime.t
; meth: [ `Stored | `Deflated ]
; crc32: Checkseum.Crc32.t
; csz: int64
; usz: int64
; offset: int64
}
type t = { filename: string; entries: entry list }
let ( let* ) = Result.bind
let error_msgf fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt
let entries { entries; _ } = entries
let really_read ic ~pos len =
In_channel.seek ic pos;
match In_channel.really_input_string ic len with
| Some str -> Ok str
| None -> error_msgf "Flux_unzip: truncated archive"
let to_ptime ~time ~date =
let ss = time land 0x1f * 2
and mm = (time lsr 5) land 0x3f
and hh = (time lsr 11) land 0x1f in
let d = date land 0x1f
and m = (date lsr 5) land 0xf
and y = 1980 + (date lsr 9) in
match Ptime.of_date_time ((y, m, d), ((hh, mm, ss), 0)) with
| Some t -> t
| None -> Ptime.epoch
let u32_to_i64 value = Int64.logand (Int64.of_int32 value) 0xffffffffL
let find_eocd ic =
let len = In_channel.length ic in
let max_scan = Int64.of_int (22 + 0xffff) in
let scan = if Int64.compare len max_scan < 0 then len else max_scan in
let pos = Int64.sub len scan in
let* tail = really_read ic ~pos (Int64.to_int scan) in
let rec go idx =
if idx < 0 then error_msgf "Flux_unzip: no end of central directory"
else if
String.get_int32_le tail idx = 0x06054b50l
&& idx + 22 + String.get_uint16_le tail (idx + 20) = String.length tail
then Ok (Int64.add pos (Int64.of_int idx), String.sub tail idx 22)
else go (idx - 1)
in
go (String.length tail - 22)
let find_central_directory ic =
let* eocd_pos, eocd = find_eocd ic in
let entries = String.get_uint16_le eocd 10 in
let cd_size = String.get_int32_le eocd 12 in
let cd_off = String.get_int32_le eocd 16 in
if entries <> 0xffff && cd_size <> 0xffffffffl && cd_off <> 0xffffffffl then
Ok (Int64.of_int entries, u32_to_i64 cd_off)
else if Int64.compare eocd_pos 20L < 0 then
error_msgf "Flux_unzip: missing zip64 locator"
else
let* locator = really_read ic ~pos:(Int64.sub eocd_pos 20L) 20 in
if String.get_int32_le locator 0 <> 0x07064b50l then
error_msgf "Flux_unzip: missing zip64 locator"
else
let eocd64_pos = String.get_int64_le locator 8 in
let* eocd64 = really_read ic ~pos:eocd64_pos 56 in
if String.get_int32_le eocd64 0 <> 0x06064b50l then
error_msgf "Flux_unzip: malformed zip64 end of central directory"
else
let entries = String.get_int64_le eocd64 32 in
let cd_off = String.get_int64_le eocd64 48 in
Ok (entries, cd_off)
let ~usz ~csz ~offset =
let rec go idx usz csz offset =
if idx + 4 > String.length extra then (usz, csz, offset)
else begin
let id = String.get_uint16_le extra idx in
let sz = String.get_uint16_le extra (idx + 2) in
if id = 0x0001 && idx + 4 + sz <= String.length extra then begin
let jdx = ref (idx + 4) in
let take () =
let value = String.get_int64_le extra !jdx in
jdx := !jdx + 8;
value
in
let usz = if usz = 0xffffffffL then take () else usz in
let csz = if csz = 0xffffffffL then take () else csz in
let offset = if offset = 0xffffffffL then take () else offset in
go (idx + 4 + sz) usz csz offset
end
else go (idx + 4 + sz) usz csz offset
end
in
go 0 usz csz offset
let parse_entry ic pos =
let* cdfh = really_read ic ~pos 46 in
if String.get_int32_le cdfh 0 <> 0x02014b50l then
error_msgf "Flux_unzip: malformed central directory"
else if String.get_uint16_le cdfh 8 land 0x1 <> 0 then
error_msgf "Flux_unzip: encrypted members are not supported"
else
let* meth =
match String.get_uint16_le cdfh 10 with
| 0 -> Ok `Stored
| 8 -> Ok `Deflated
| meth -> error_msgf "Flux_unzip: unsupported compression method %d" meth
in
let time = String.get_uint16_le cdfh 12
and date = String.get_uint16_le cdfh 14 in
let crc32 = Optint.of_unsigned_int32 (String.get_int32_le cdfh 16) in
let csz = u32_to_i64 (String.get_int32_le cdfh 20) in
let usz = u32_to_i64 (String.get_int32_le cdfh 24) in
let name_len = String.get_uint16_le cdfh 28
and = String.get_uint16_le cdfh 30
and = String.get_uint16_le cdfh 32 in
let offset = u32_to_i64 (String.get_int32_le cdfh 42) in
let* filepath = really_read ic ~pos:(Int64.add pos 46L) name_len in
let* =
really_read ic
~pos:(Int64.add pos (Int64.of_int (46 + name_len)))
extra_len
in
let usz, csz, offset = parse_extra ~usz ~csz ~offset extra in
let mtime = to_ptime ~time ~date in
let next =
Int64.add pos (Int64.of_int (46 + name_len + extra_len + comment_len))
in
Ok ({ filepath; mtime; meth; crc32; csz; usz; offset }, next)
let of_filename filename =
let process ic =
let* entries, cd_off = find_central_directory ic in
let rec go acc pos = function
| 0L -> Ok (List.rev acc)
| n ->
let* entry, next = parse_entry ic pos in
go (entry :: acc) next (Int64.pred n)
in
let* entries = go [] cd_off entries in
Ok { filename; entries }
in
match In_channel.open_bin filename with
| ic ->
Fun.protect ~finally:(fun () -> In_channel.close ic) @@ fun () ->
process ic
| exception Sys_error err -> error_msgf "Flux_unzip: %s" err
let raw ~filename ~pos ~len : string Flux.source =
let init () =
let ic = In_channel.open_bin filename in
match really_read ic ~pos 30 with
| Error (`Msg msg) -> In_channel.close ic; failwith msg
| Ok lfh ->
if String.get_int32_le lfh 0 <> 0x04034b50l then begin
In_channel.close ic;
failwith "Flux_unzip: malformed local file header"
end;
let name_len = String.get_uint16_le lfh 26
and = String.get_uint16_le lfh 28 in
let data = Int64.add pos (Int64.of_int (30 + name_len + extra_len)) in
In_channel.seek ic data; (ic, len)
and pull (ic, rem) =
if Int64.compare rem 0L <= 0 then None
else
let len =
if Int64.compare rem 0x7ffL < 0 then Int64.to_int rem else 0x7ff
in
match In_channel.really_input_string ic len with
| Some str -> Some (str, (ic, Int64.sub rem (Int64.of_int len)))
| None -> failwith "Flux_unzip: truncated archive"
and stop (ic, _) = In_channel.close ic in
Flux.Source { init; pull; stop }
let stream t entry =
let raw = raw ~filename:t.filename ~pos:entry.offset ~len:entry.csz in
let via =
match entry.meth with
| `Stored -> Flux.Flow.identity
| `Deflated -> Flux.Flow.(bstr ~len:0x7ff << Flux_de.inflate)
in
let stream (Flux.Sink k) =
let crc32 = ref Checkseum.Crc32.default in
let seen = ref 0L in
let push acc str =
let len = String.length str in
crc32 := Checkseum.Crc32.digest_string str 0 len !crc32;
seen := Int64.add !seen (Int64.of_int len);
k.push acc str
and stop acc =
if Int64.equal !seen entry.usz && not (Optint.equal !crc32 entry.crc32)
then Fmt.failwith "Flux_unzip: CRC-32 mismatch for %s" entry.filepath;
k.stop acc
in
let into = Flux.Sink { init= k.init; push; full= k.full; stop } in
let value, leftover = Flux.Stream.run ~from:raw ~via ~into in
Option.iter Flux.Source.dispose leftover;
value
in
{ Flux.stream }