1 (***********************************************************************)
3 (* The CamlZip library *)
5 (* Xavier Leroy, projet Cristal, INRIA Rocquencourt *)
7 (* Copyright 2001 Institut National de Recherche en Informatique et *)
8 (* en Automatique. All rights reserved. This file is distributed *)
9 (* under the terms of the GNU Library General Public License. *)
11 (***********************************************************************)
15 (* Module [Zip]: reading and writing ZIP archives *)
17 exception Error
of string * string * string
19 let read1 = input_byte
21 let lb = read1 ic
in let hb = read1 ic
in lb lor (hb lsl 8)
23 let lw = read2 ic
in let hw = read2 ic
in
24 Int32.logor
(Int32.of_int
lw) (Int32.shift_left
(Int32.of_int
hw) 16)
26 let lw = read2 ic
in let hw = read2 ic
in
27 if hw > max_int
lsr 16 then raise
(Error
("", "", "32-bit data too large"));
30 let s = String.create n
in
31 really_input ic
s 0 n
; s
33 let write1 = output_byte
35 write1 oc n
; write1 oc
(n
lsr 8)
37 write2 oc
(Int32.to_int n
);
38 write2 oc
(Int32.to_int
(Int32.shift_right_logical n
16))
42 let writestring oc
s =
43 output oc
s 0 (String.length
s)
45 type compression_method
= Stored
| Deflated
51 methd
: compression_method
;
54 uncompressed_size
: int;
60 { if_filename
: string;
61 if_channel
: Pervasives.in_channel
;
62 if_entries
: entry list
;
63 if_directory
: (string, entry
) Hashtbl.t
;
66 let entries ifile
= ifile
.if_entries
67 let comment ifile
= ifile
.if_comment
70 { of_filename
: string;
71 of_channel
: Pervasives.out_channel
;
72 mutable of_entries
: entry list
;
75 exception Error
of string * string * string
77 (* Return the position of the last occurrence of s1 in s2, or -1 if not
80 let strrstr pattern buf ofs len
=
83 else if j
>= String.length pattern
then i
84 else if pattern
.[j
] = buf
.[i
+ j
] then search i
(j
+1)
86 in search (ofs
+ len
- String.length pattern
) 0
88 (* Determine if a file name is a directory (ends with /) *)
90 let filename_is_directory name
=
91 String.length name
> 0 && name
.[String.length name
- 1] = '
/'
93 (* Convert between Unix dates and DOS dates *)
95 let unixtime_of_dostime time date
=
97 { Unix.tm_sec
= (time
lsl 1) land 0x3e;
98 Unix.tm_min
= (time
lsr 5) land 0x3f;
99 Unix.tm_hour
= (time
lsr 11) land 0x1f;
100 Unix.tm_mday
= date
land 0x1f;
101 Unix.tm_mon
= ((date
lsr 5) land 0xf) - 1;
102 Unix.tm_year
= ((date
lsr 9) land 0x7f) + 80;
105 Unix.tm_isdst
= false })
107 let dostime_of_unixtime t
=
108 let tm = Unix.localtime t
in
109 (tm.Unix.tm_sec
lsr 1
110 + (tm.Unix.tm_min
lsl 5)
111 + (tm.Unix.tm_hour
lsl 11),
113 + (tm.Unix.tm_mon
+ 1) lsl 5
114 + (tm.Unix.tm_year
- 80) lsl 9)
116 (* Read end of central directory record *)
118 let read_ecd filename ic
=
119 let buf = String.create
256 in
120 let filelen = in_channel_length ic
in
121 let rec find_ecd pos len
=
122 (* On input, bytes 0 ... len - 1 of buf reflect what is at pos in ic *)
123 if pos
<= 0 || filelen - pos
>= 0x10000 then
124 raise
(Error
(filename
, "",
125 "end of central directory not found, not a ZIP file"));
126 let toread = min pos
128 in
127 (* Make room for "toread" extra bytes, and read them *)
128 String.blit
buf 0 buf toread (256 - toread);
129 let newpos = pos
- toread in
131 really_input ic
buf 0 toread;
132 let newlen = min
(toread + len
) 256 in
133 (* Search for magic number *)
134 let ofs = strrstr "PK\005\006" buf 0 newlen in
135 if ofs < 0 || newlen < 22 ||
137 Char.code
buf.[ofs + 20] lor (Char.code
buf.[ofs + 21] lsl 8) in
138 newpos + ofs + 22 + comment_len <> filelen) then
139 find_ecd newpos newlen
142 seek_in ic
(find_ecd filelen 0);
143 let magic = read4 ic
in
144 let disk_no = read2 ic
in
145 let cd_disk_no = read2 ic
in
146 let _disk_entries = read2 ic
in
147 let cd_entries = read2 ic
in
148 let cd_size = read4 ic
in
149 let cd_offset = read4 ic
in
150 let comment_len = read2 ic
in
151 let comment = readstring ic
comment_len in
152 assert (magic = Int32.of_int
0x06054b50);
153 if disk_no <> 0 || cd_disk_no <> 0 then
154 raise
(Error
(filename
, "", "multi-disk ZIP files not supported"));
155 (cd_entries, cd_size, cd_offset, comment)
157 (* Read central directory *)
159 let read_cd filename ic
cd_entries cd_offset cd_bound
=
160 let cd_bound = Int64.of_int32
cd_bound in
162 LargeFile.seek_in ic
(Int64.of_int32
cd_offset);
164 let entrycnt = ref 0 in
165 while (LargeFile.pos_in ic
< cd_bound) do
167 let magic = read4 ic
in
168 let _version_made_by = read2 ic
in
169 let _version_needed = read2 ic
in
170 let flags = read2 ic
in
171 let methd = read2 ic
in
172 let lastmod_time = read2 ic
in
173 let lastmod_date = read2 ic
in
174 let crc = read4 ic
in
175 let compr_size = read4_int ic
in
176 let uncompr_size = read4_int ic
in
177 let name_len = read2 ic
in
178 let extra_len = read2 ic
in
179 let comment_len = read2 ic
in
180 let _disk_number = read2 ic
in
181 let _internal_attr = read2 ic
in
182 let _external_attr = read4 ic
in
183 let header_offset = Int64.of_int32
(read4 ic
) in
184 let name = readstring ic
name_len in
185 let extra = readstring ic
extra_len in
186 let comment = readstring ic
comment_len in
187 if magic <> Int32.of_int
0x02014b50 then
188 raise
(Error
(filename
, name,
189 "wrong file header in central directory"));
190 if flags land 1 <> 0 then
191 raise
(Error
(filename
, name, "encrypted entries not supported"));
193 e := { filename
= name;
196 methd = (match methd with
199 | _
-> raise
(Error
(filename
, name,
200 "unknown compression method")));
201 mtime
= unixtime_of_dostime lastmod_time lastmod_date;
203 uncompressed_size
= uncompr_size;
204 compressed_size
= compr_size;
205 is_directory
= filename_is_directory name;
206 file_offset
= header_offset
209 assert((cd_bound = (LargeFile.pos_in ic
)) &&
210 (cd_entries = 65535 || !entrycnt = cd_entries));
213 raise
(Error
(filename
, "", "end-of-file while reading central directory"))
215 (* Open a ZIP file for reading *)
217 let open_in filename
=
218 let ic = Pervasives.open_in_bin filename
in
219 let (cd_entries, cd_size, cd_offset, cd_comment
) = read_ecd filename
ic in
221 read_cd filename
ic cd_entries cd_offset (Int32.add
cd_offset cd_size) in
222 let dir = Hashtbl.create
(cd_entries / 3) in
223 List.iter
(fun e -> Hashtbl.add
dir e.filename
e) entries;
224 { if_filename
= filename
;
226 if_entries
= entries;
228 if_comment
= cd_comment
}
230 (* Close a ZIP file opened for reading *)
233 Pervasives.close_in ifile
.if_channel
235 (* Return the info associated with an entry *)
237 let find_entry ifile
name =
238 Hashtbl.find ifile
.if_directory
name
240 (* Position on an entry *)
242 let goto_entry ifile
e =
244 let ic = ifile
.if_channel
in
245 LargeFile.seek_in
ic e.file_offset
;
246 let magic = read4 ic in
247 let _version_needed = read2 ic in
248 let _flags = read2 ic in
249 let _methd = read2 ic in
250 let _lastmod_time = read2 ic in
251 let _lastmod_date = read2 ic in
252 let _crc = read4 ic in
253 let _compr_size = read4_int ic in
254 let _uncompr_size = read4_int ic in
255 let filename_len = read2 ic in
256 let extra_len = read2 ic in
257 if magic <> Int32.of_int
0x04034b50 then
258 raise
(Error
(ifile
.if_filename
, e.filename
, "wrong local file header"));
259 (* Could validate information read against directory entry, but
261 LargeFile.seek_in ifile
.if_channel
262 (Int64.add
e.file_offset
(Int64.of_int
(30 + filename_len + extra_len)))
264 raise
(Error
(ifile
.if_filename
, e.filename
, "truncated local file header"))
266 (* Read the contents of an entry as a string *)
268 let read_entry ifile
e =
271 let res = String.create
e.uncompressed_size
in
274 if e.compressed_size
<> e.uncompressed_size
then
275 raise
(Error
(ifile
.if_filename
, e.filename
,
276 "wrong size for stored entry"));
277 really_input ifile
.if_channel
res 0 e.uncompressed_size
;
280 let in_avail = ref e.compressed_size
in
281 let out_pos = ref 0 in
283 Zlib.uncompress ~header
:false
285 let read = input ifile
.if_channel
buf 0
286 (min
!in_avail (String.length
buf)) in
287 in_avail := !in_avail - read;
290 if !out_pos + len
> String.length
res then
291 raise
(Error
(ifile
.if_filename
, e.filename
,
292 "wrong size for deflated entry (too much data)"));
293 String.blit
buf 0 res !out_pos len
;
294 out_pos := !out_pos + len
)
295 with Zlib.Error
(_
, _
) ->
296 raise
(Error
(ifile
.if_filename
, e.filename
, "decompression error"))
298 if !out_pos <> String.length
res then
299 raise
(Error
(ifile
.if_filename
, e.filename
,
300 "wrong size for deflated entry (not enough data)"));
301 let crc = Zlib.update_crc
Int32.zero
res 0 (String.length
res) in
303 raise
(Error
(ifile
.if_filename
, e.filename
, "CRC mismatch"));
306 raise
(Error
(ifile
.if_filename
, e.filename
, "truncated data"))
308 (* Write the contents of an entry into an out channel *)
310 let copy_entry_to_channel ifile
e oc
=
315 if e.compressed_size
<> e.uncompressed_size
then
316 raise
(Error
(ifile
.if_filename
, e.filename
,
317 "wrong size for stored entry"));
318 let buf = String.create
4096 in
321 let r = input ifile
.if_channel
buf 0 (min n
(String.length
buf)) in
325 copy e.uncompressed_size
327 let in_avail = ref e.compressed_size
in
328 let crc = ref Int32.zero
in
330 Zlib.uncompress ~header
:false
332 let read = input ifile
.if_channel
buf 0
333 (min
!in_avail (String.length
buf)) in
334 in_avail := !in_avail - read;
338 crc := Zlib.update_crc
!crc buf 0 len
)
339 with Zlib.Error
(_
, _
) ->
340 raise
(Error
(ifile
.if_filename
, e.filename
, "decompression error"))
342 if !crc <> e.crc then
343 raise
(Error
(ifile
.if_filename
, e.filename
, "CRC mismatch"))
345 raise
(Error
(ifile
.if_filename
, e.filename
, "truncated data"))
347 (* Write the contents of an entry to a file *)
349 let copy_entry_to_file ifile
e outfilename
=
350 let oc = open_out_bin outfilename
in
352 copy_entry_to_channel ifile
e oc;
355 Unix.utimes outfilename
e.mtime
e.mtime
356 with Unix.Unix_error
(_
, _
, _
) | Invalid_argument _
-> ()
360 Sys.remove outfilename
;
363 (* Open a ZIP file for writing *)
365 let open_out ?
(comment = "") filename
=
366 if String.length
comment >= 0x10000 then
367 raise
(Error
(filename
, "", "comment too long"));
368 { of_filename
= filename
;
369 of_channel
= Pervasives.open_out_bin filename
;
371 of_comment
= comment }
373 (* Close a ZIP file for writing. Add central directory. *)
375 let write_directory_entry oc e =
376 write4 oc (Int32.of_int
0x02014b50); (* signature *)
377 let version = match e.methd with Stored
-> 10 | Deflated
-> 20 in
378 write2 oc version; (* version made by *)
379 write2 oc version; (* version needed to extract *)
380 write2 oc 8; (* flags *)
381 write2 oc (match e.methd with Stored
-> 0 | Deflated
-> 8); (* method *)
382 let (time
, date
) = dostime_of_unixtime e.mtime
in
383 write2 oc time
; (* last mod time *)
384 write2 oc date
; (* last mod date *)
385 write4 oc e.crc; (* CRC32 *)
386 write4_int oc e.compressed_size
; (* compressed size *)
387 write4_int oc e.uncompressed_size
; (* uncompressed size *)
388 write2 oc (String.length
e.filename
); (* filename length *)
389 write2 oc (String.length
e.extra); (* extra length *)
390 write2 oc (String.length
e.comment); (* comment length *)
391 write2 oc 0; (* disk number start *)
392 write2 oc 0; (* internal attributes *)
393 write4_int oc 0; (* external attributes *)
394 write4 oc (Int64.to_int32
e.file_offset
); (* offset of local header *)
395 writestring oc e.filename
; (* filename *)
396 writestring oc e.extra; (* extra info *)
397 writestring oc e.comment (* file comment *)
399 let close_out ofile
=
400 let oc = ofile
.of_channel
in
401 let start_cd = pos_out
oc in
402 List.iter
(write_directory_entry oc) (List.rev ofile
.of_entries
);
403 let cd_size = pos_out
oc - start_cd in
404 let num_entries = List.length ofile
.of_entries
in
405 if num_entries >= 0x10000 then
406 raise
(Error
(ofile
.of_filename
, "", "too many entries"));
407 write4 oc (Int32.of_int
0x06054b50); (* signature *)
408 write2 oc 0; (* disk number *)
409 write2 oc 0; (* number of disk with central dir *)
410 write2 oc num_entries; (* # entries in this disk *)
411 write2 oc num_entries; (* # entries in central dir *)
412 write4_int oc cd_size; (* size of central dir *)
413 write4_int oc start_cd; (* offset of central dir *)
414 write2 oc (String.length ofile
.of_comment
); (* length of comment *)
415 writestring oc ofile
.of_comment
; (* comment *)
416 Pervasives.close_out oc
418 (* Write a local file header and return the corresponding entry *)
420 let add_entry_header ofile
extra comment level mtime filename
=
421 if level
< 0 || level
> 9 then
422 raise
(Error
(ofile
.of_filename
, filename
, "wrong compression level"));
423 if String.length filename
>= 0x10000 then
424 raise
(Error
(ofile
.of_filename
, filename
, "filename too long"));
425 if String.length
extra >= 0x10000 then
426 raise
(Error
(ofile
.of_filename
, filename
, "extra data too long"));
427 if String.length
comment >= 0x10000 then
428 raise
(Error
(ofile
.of_filename
, filename
, "comment too long"));
429 let oc = ofile
.of_channel
in
430 let pos = LargeFile.pos_out
oc in
431 write4 oc (Int32.of_int
0x04034b50); (* signature *)
432 let version = if level
= 0 then 10 else 20 in
433 write2 oc version; (* version needed to extract *)
434 write2 oc 8; (* flags *)
435 write2 oc (if level
= 0 then 0 else 8); (* method *)
436 let (time
, date
) = dostime_of_unixtime mtime
in
437 write2 oc time
; (* last mod time *)
438 write2 oc date
; (* last mod date *)
439 write4 oc Int32.zero
; (* CRC32 - to be filled later *)
440 write4_int oc 0; (* compressed size - later *)
441 write4_int oc 0; (* uncompressed size - later *)
442 write2 oc (String.length filename
); (* filename length *)
443 write2 oc (String.length
extra); (* extra length *)
444 writestring oc filename
; (* filename *)
445 writestring oc extra; (* extra info *)
446 { filename
= filename
;
449 methd = (if level
= 0 then Stored
else Deflated
);
452 uncompressed_size
= 0;
454 is_directory
= filename_is_directory filename
;
457 (* Write a data descriptor and update the entry *)
459 let add_data_descriptor ofile
crc compr_size uncompr_size entry
=
460 let oc = ofile
.of_channel
in
461 write4 oc (Int32.of_int
0x08074b50); (* signature *)
462 write4 oc crc; (* CRC *)
463 write4_int oc compr_size; (* compressed size *)
464 write4_int oc uncompr_size; (* uncompressed size *)
465 { entry
with crc = crc;
466 uncompressed_size
= uncompr_size;
467 compressed_size
= compr_size }
469 (* Add an entry with the contents of a string *)
471 let add_entry data ofile ?
(extra = "") ?
(comment = "")
472 ?
(level
= 6) ?
(mtime
= Unix.time
()) name =
473 let e = add_entry_header ofile
extra comment level mtime
name in
474 let crc = Zlib.update_crc
Int32.zero data
0 (String.length data
) in
478 output ofile
.of_channel data
0 (String.length data
);
481 let in_pos = ref 0 in
482 let out_pos = ref 0 in
484 Zlib.compress ~header
:false
486 let n = min
(String.length data
- !in_pos)
487 (String.length
buf) in
488 String.blit data
!in_pos buf 0 n;
489 in_pos := !in_pos + n;
492 output ofile
.of_channel
buf 0 n;
493 out_pos := !out_pos + n);
495 with Zlib.Error
(_
, _
) ->
496 raise
(Error
(ofile
.of_filename
, name, "compression error")) in
497 let e'
= add_data_descriptor ofile
crc compr_size (String.length data
) e in
498 ofile
.of_entries
<- e'
:: ofile
.of_entries
500 (* Add an entry with the contents of an in channel *)
502 let copy_channel_to_entry ic ofile ?
(extra = "") ?
(comment = "")
503 ?
(level
= 6) ?
(mtime
= Unix.time
()) name =
504 let e = add_entry_header ofile
extra comment level mtime
name in
505 let crc = ref Int32.zero
in
506 let (compr_size, uncompr_size) =
509 let buf = String.create
4096 in
511 let r = input
ic buf 0 (String.length
buf) in
512 if r = 0 then sz
else begin
513 crc := Zlib.update_crc
!crc buf 0 r;
514 output ofile
.of_channel
buf 0 r;
520 let in_pos = ref 0 in
521 let out_pos = ref 0 in
523 Zlib.compress ~header
:false
525 let r = input
ic buf 0 (String.length
buf) in
526 crc := Zlib.update_crc
!crc buf 0 r;
527 in_pos := !in_pos + r;
530 output ofile
.of_channel
buf 0 n;
531 out_pos := !out_pos + n);
533 with Zlib.Error
(_
, _
) ->
534 raise
(Error
(ofile
.of_filename
, name, "compression error")) in
535 let e'
= add_data_descriptor ofile
!crc compr_size uncompr_size e in
536 ofile
.of_entries
<- e'
:: ofile
.of_entries
538 (* Add an entry with the contents of a file *)
540 let copy_file_to_entry infilename ofile ?
(extra = "") ?
(comment = "")
541 ?
(level
= 6) ?mtime
name =
542 let ic = open_in_bin infilename
in
547 try Some
((Unix.LargeFile.stat infilename
).Unix.LargeFile.st_mtime
)
548 with Unix.Unix_error
(_
,_
,_
) -> None
in
550 copy_channel_to_entry ic ofile ~
extra ~
comment ~level ?
mtime:mtime'
name;
551 Pervasives.close_in ic
553 Pervasives.close_in ic; raise x