Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Page
Library
Module
Module type
Parameter
Class
Class type
Source
zipc.ml1 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 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588(*--------------------------------------------------------------------------- Copyright (c) 2023 The zipc programmers. All rights reserved. SPDX-License-Identifier: ISC ---------------------------------------------------------------------------*) (* The write up at https://www.hanshq.net/zip.html helps understanding the bits here. *) (* Preliminaries *) let ( let* ) = Result.bind let strf fmt = Format.asprintf fmt let failwithf fmt = Printf.ksprintf failwith fmt let invalid_argf fmt = Printf.ksprintf invalid_arg fmt let default_length ?len s start = match len with | None -> String.length s - start | Some size -> size let uint32_max_or_max_int = match Int32.unsigned_to_int 0xFFFFFFFFl with | None -> Int.max_int | Some max -> max (* Archive members *) type compression = Bzip2 | Deflate | Lzma | Stored | Xz | Zstd | Other of int let compression_of_int = function | 0 -> Stored | 8 -> Deflate | 12 -> Bzip2 | 14 -> Lzma | 93 -> Zstd | 95 -> Xz | c -> Other c let compression_to_int = function | Stored -> 0 | Deflate -> 8 | Bzip2 -> 12 | Lzma -> 14 | Zstd -> 93 | Xz -> 95 | Other c -> c let compression_to_string = function | Bzip2 -> "bz2" | Deflate -> "defl" | Lzma -> "lzma" | Stored -> "none" | Xz -> "xz" | Zstd -> "zst" | Other i -> Format.sprintf "%04d" i let pp_compression ppf c = Format.pp_print_string ppf (compression_to_string c) module Fpath = struct type t = string let ensure_unix p = String.map (function '\\' -> '/' | c -> c) p let ensure_directoryness = function | "" -> "./" | p when p.[String.length p - 1] = '/' -> p | p -> p ^ "/" let sanitize p = let keep_seg = function "" | ".." | "." -> false | _ -> true in let segs = String.split_on_char '/' p in let segs = List.concat_map (String.split_on_char '\\') segs in String.concat "/" (List.filter keep_seg segs) type mode = int let pp_mode ppf m = let pp_entity ppf m = let r = if (m land 0o4 <> 0) then 'r' else '-' in let w = if (m land 0o2 <> 0) then 'w' else '-' in let x = if (m land 0o1 <> 0) then 'x' else '-' in Format.fprintf ppf "%c%c%c" r w x in pp_entity ppf (m lsr 6); pp_entity ppf (m lsr 3); pp_entity ppf m end module Ptime = struct type t = int let jd_posix_epoch = 2_440_588 (* the Julian day of the POSIX epoch *) let to_date_time ptime_s = let jd = (ptime_s / 86400) + jd_posix_epoch in let jd_rem = ptime_s mod 86400 in let hh = jd_rem / 3600 in let hh_rem = jd_rem mod 3600 in let mm = hh_rem / 60 in let ss = hh_rem mod 60 in let date = (* Date of julian day cf. ptime *) let a = jd + 32044 in let b = (4 * a + 3) / 146097 in let c = a - ((146097 * b) / 4) in let d = (4 * c + 3) / 1461 in let e = c - ((1461 * d) / 4) in let m = (5 * e + 2) / 153 in let day = e - ((153 * m + 2) / 5) + 1 in let month = m + 3 - (12 * (m / 10)) in let year = 100 * b + d - 4800 + (m / 10) in (year, month, day) in date, (hh, mm, ss) let pp ppf ptime = (* Like RFC3339 without the T separator *) let (year, month, day), (hh, mm, ss) = to_date_time ptime in Format.fprintf ppf "%04d-%02d-%02d %02d:%02d:%02dZ" year month day hh mm ss (* MS-DOS date time https://www.ctyme.com/intr/rb-2992.htm#table1665 *) let dos_epoch = 315532800 (* in POSIX time. *) let of_dos_date_time ~dos_date ~dos_time = if dos_date < 0x21 (* 1980-01-01 *) then dos_epoch else let hh = (dos_time lsr 11) in let mm = (dos_time lsr 5) land 0x3F in let ss = (dos_time land 0x1F) * 2 in let jd = (* Julian day of date *) let year = ((dos_date lsr 9) land 0x7F) + 1980 in let month = ((dos_date lsr 5) land 0xF) in let day = (dos_date land 0x1F) in (* Cf. ptime *) let a = (14 - month) / 12 in let y = year + 4800 - a in let m = month + 12 * a - 3 in day + ((153 * m) + 2) / 5 + 365 * y + (y / 4) - (y / 100) + (y / 400) - 32045 in let d = jd - jd_posix_epoch in d * 86400 + hh * 3600 + mm * 60 + ss let to_dos_date_time ptime_s = let ((y, _, _), _ as date_time) = to_date_time ptime_s in let (year, month, day), (hh, mm, ss) = if y < 1980 then (1980, 01, 01), (00, 00, 00) else if y > 2107 then (2107, 12, 31), (23, 59, 59) else date_time in let dos_date = day lor (month lsl 5) lor ((year - 1980) lsl 9) in let dos_time = (ss / 2) lor (mm lsl 5) lor (hh lsl 11) in dos_date, dos_time end module File = struct let err_format c = strf "Compression %a not supported" pp_compression c let err_encrypted = "Encrypted file not supported" let err_size compressed_size decompressed_size = strf "Maximum ZIP byte size 4294967295 exceeded by compressed \ (%d) or decompressed (%d) file size" compressed_size decompressed_size let check_non_negative name len = if len < 0 then invalid_argf "%s is negative (%d)" name len let max_size = uint32_max_or_max_int let gp_is_encrypted = 0x1 let gp_utf_8 = 0x800 let gp_default = gp_utf_8 let version_made_by_default = (3 (* UNIX *) lsl 8) lor 20 (* PKZIP 2.0 *) let version_needed_to_extract_default = 20 (* PKZIP 2.0 *) type t = { version_made_by : Zipc_deflate.uint16; version_needed_to_extract : Zipc_deflate.uint16; gp_flags : Zipc_deflate.uint16; compression : compression; (* in [compressed_bytes] buffer. *) start : int; (* in [compressed_bytes] buffer. *) compressed_size : int; (* in [compressed_bytes] in buffer. *) compressed_bytes : string; decompressed_size : int; decompressed_crc_32 : Zipc_deflate.Crc_32.t } let make ?(version_made_by = version_made_by_default) ?(version_needed_to_extract = version_needed_to_extract_default) ?(gp_flags = gp_default) ?(start = 0) ?compressed_size:len ~compression compressed_bytes ~decompressed_size ~decompressed_crc_32 = let compressed_size = default_length ?len compressed_bytes start in check_non_negative "compressed_size" compressed_size; check_non_negative "decompressed_size" decompressed_size; if compressed_size > max_size || decompressed_size > max_size then Error (err_size compressed_size decompressed_size) else Ok { version_made_by; version_needed_to_extract; gp_flags; compression; start; compressed_size; compressed_bytes; decompressed_size; decompressed_crc_32; } let stored_of_binary_string ?(start = 0) ?len s = let compression = Stored in let decompressed_size = default_length ?len s start in let decompressed_crc_32 = Zipc_deflate.Crc_32.string ~start ?len s in let compressed_size = decompressed_size in make ~start ~compressed_size ~compression s ~decompressed_size ~decompressed_crc_32 let deflate_of_binary_string ?level ?(start = 0) ?len s = let compression = Deflate in let decompressed_size = default_length ?len s start in let* decompressed_crc_32, cs = Zipc_deflate.crc_32_and_deflate ?level ~start ?len s in make ~compression cs ~decompressed_size ~decompressed_crc_32 let version_made_by file = file.version_made_by let version_needed_to_extract file = file.version_needed_to_extract let gp_flags file = file.gp_flags let compression file = file.compression let start file = file.start let compressed_size file = file.compressed_size let compressed_bytes file = file.compressed_bytes let decompressed_size file = file.decompressed_size let decompressed_crc_32 file = file.decompressed_crc_32 let compressed_bytes_to_binary_string file = String.sub file.compressed_bytes file.start file.compressed_size let is_encrypted file = file.gp_flags land gp_is_encrypted <> 0 let can_extract file = not (is_encrypted file) && match file.compression with | Stored | Deflate -> true | _ -> false let to_binary_string_no_crc_check file = if is_encrypted file then Error err_encrypted else match file.compression with | Stored -> let s = compressed_bytes_to_binary_string file in Ok (s, Zipc_deflate.Crc_32.string s) | Deflate -> let cs = file.compressed_bytes in let start = file.start and len = file.compressed_size in let decompressed_size = file.decompressed_size in Result.map_error (strf "deflate: %s") @@ Zipc_deflate.inflate_and_crc_32 cs ~start ~len ~decompressed_size | c -> Error (err_format c) let to_binary_string file = match to_binary_string_no_crc_check file with | Error _ as e -> e | Ok (s, found) -> let expect = file.decompressed_crc_32 in match Zipc_deflate.Crc_32.check ~expect ~found with | Error _ as e -> e | Ok () -> Ok s end module Member = struct let max = 0xFFFF let max_path_length = 0xFFFF let err_count count = strf "Maximum ZIP member count %d exceeded (%d)" max count let err_path_len len = strf "Maximum ZIP path length %d exceeded (%d)" max_path_length len type kind = Dir | File of File.t type t = { path : Fpath.t; kind : kind; mode : Fpath.mode; mtime : Ptime.t } let make ?(mtime = Ptime.dos_epoch) ?mode ~path kind = let path = Fpath.ensure_unix path in let path = match kind with | Dir -> Fpath.ensure_directoryness path | File _ -> path in let path_len = String.length path in if path_len > max_path_length then Error (err_path_len path_len) else let mode = match mode with | Some m -> m | None -> match kind with Dir -> 0o755 | File _ -> 0o644 in let mtime = if mtime < Ptime.dos_epoch then Ptime.dos_epoch else mtime in Ok { path; kind; mode; mtime } let path m = m.path let kind m = m.kind let mode m = m.mode let mtime m = m.mtime let _pp ~crc ppf m = let is_dir = match m.kind with Dir -> 'd' | File _ -> '-' in let comp = match m.kind with | Dir -> "none" | File f -> strf "%4s" (compression_to_string f.compression) in let encrypted = match m.kind with | File f when File.is_encrypted f -> 'X' | _ -> ' ' in let size = match m.kind with Dir -> 0 | File f -> f.decompressed_size in let pct = match m.kind with | Dir -> " " | File f -> let r = (float f.compressed_size) /. (float f.decompressed_size) in strf "%3d%%" (Float.to_int (r *. 100.)) in let crc = if not crc then "" else match m.kind with | Dir -> " " | File f -> strf "%08lx" f.decompressed_crc_32 in Format.fprintf ppf "%c%a %s%c%s %8d %s %a %s" is_dir Fpath.pp_mode m.mode comp encrypted crc size pct Ptime.pp m.mtime m.path let pp = _pp ~crc:false let pp_long = _pp ~crc:true end let error_zip64 () = failwith "ZIP64 archives are not supported" let error_multipart () = failwith "Multipart archives are not supported" let error_eocd () = failwith "Corrupted end of central directory record" let error_no_eocd () = failwith "Likely not a ZIP archive: no end of central directory record found" let error_short () = failwith "File too short to be a ZIP archive" let error_truncated_cd () = failwith "Truncated central directory" let error_corrupted_cdfh () = failwith "Corrupted central directory file header" let error_corrupted_lfh () = failwith "Corrupted local file header" module String_map = Map.Make (String) type t = Member.t String_map.t let empty = String_map.empty let is_empty = String_map.is_empty let mem p z = String_map.mem p z let find p z = String_map.find_opt p z let fold f z acc = String_map.fold (fun _ m acc -> f m acc) z acc let add m z = String_map.add m.Member.path m z let remove p z = String_map.remove p z let member_count z = String_map.cardinal z let to_string_map = Fun.id let of_string_map = Fun.id (* Decoding The spec is https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT but https://en.wikipedia.org/wiki/ZIP_(file_format) has tables with offsets which makes it easier to understand the indexes below. *) let get_uint32_le_as_int s i = let u = String.get_int32_le s i in match Int32.unsigned_to_int u with | Some u -> u | None -> failwithf "Cannot convert 32-bit %lu to OCaml int" u let lfh_sig = 0x04034b50l let lfh_min_size = 30 let decode_data_start_of_lfh s i ~compressed_size = let len = String.length s in if i + lfh_min_size > len || String.get_int32_le s i <> lfh_sig then error_corrupted_lfh () else let path_len = String.get_uint16_le s (i + 26) in let extra_len = String.get_uint16_le s (i + 28) in let data_start = i + lfh_min_size + path_len + extra_len in if data_start + compressed_size > len then error_corrupted_lfh () else data_start let get_lfh_crc_32 s ~start_local = (* assert decode_data_start_of_lfh has been called *) String.get_int32_le s (start_local + 14) let cdfh_sig = 0x02014b50l let cdfh_min_size = 46 let decode_member_of_cd s cd_max i = if i + cdfh_min_size - 1 > cd_max || String.get_int32_le s i <> cdfh_sig then error_corrupted_cdfh () else let path_len = String.get_uint16_le s (i + 28) in let next_member_idx = let extra_len = String.get_uint16_le s (i + 30) in let comment_len = String.get_uint16_le s (i + 32) in let n = i + cdfh_min_size + path_len + extra_len + comment_len in if n - 1 > cd_max then error_corrupted_cdfh () else n in let path = String.sub s (i + 46) path_len in let mtime = let dos_time = String.get_uint16_le s (i + 12) in let dos_date = String.get_uint16_le s (i + 14) in Ptime.of_dos_date_time ~dos_date ~dos_time in let is_dir, mode = let ext_atts_16_hi = String.get_uint16_le s (i + 40) in if ext_atts_16_hi <> 0 then (* Unix permissions *) (ext_atts_16_hi land 0o70000) = 0o40000, (ext_atts_16_hi land 0o07777) else (* check for MS-DOS directory bit *) let ext_atts_8_lo = String.get_uint8 s (i + 38) in if ext_atts_8_lo land 0x10 <> 0 then (true, 0o755) else (false, 0o644) in let kind = if is_dir then Member.Dir else let compression = compression_of_int (String.get_uint16_le s (i + 10)) in let version_made_by = String.get_uint16_le s (i + 4) in let version_needed_to_extract = String.get_uint16_le s (i + 6) in let gp_flags = String.get_uint16_le s (i + 8) in let compressed_size = get_uint32_le_as_int s (i + 20) in let decompressed_size = get_uint32_le_as_int s (i + 24) in let decompressed_crc_32 = String.get_int32_le s (i + 16) in let start_local = get_uint32_le_as_int s (i + 42) in if start_local >= String.length s then error_corrupted_cdfh () else let start = decode_data_start_of_lfh s start_local ~compressed_size in let decompressed_crc_32 = if decompressed_crc_32 = 0l then get_lfh_crc_32 s ~start_local else decompressed_crc_32 in File { version_made_by; version_needed_to_extract; gp_flags; compression; compressed_bytes = s; start; compressed_size; decompressed_size; decompressed_crc_32 } in next_member_idx, { Member.path; mtime; mode; kind } let rec decode_cd_members z count s cd_max i = if count = 0 then z else if i > cd_max then error_truncated_cd () else let i, member = decode_member_of_cd s cd_max i in decode_cd_members (add member z) (count - 1) s cd_max i let eocd_sig = 0x06054b50l let eocd_min_size = 22 let find_cd_info_in_eocd s = let info_of_eocd s i = let disk_num = String.get_uint16_le s (i + 4) in let disk_num_of_cd_start = String.get_uint16_le s (i + 6) in if disk_num = 0xFFFF then error_zip64 () else if disk_num <> 0 then error_multipart () else if disk_num_of_cd_start <> 0 then error_multipart () else let cd_member_count = String.get_uint16_le s (i + 10) in let cd_size = get_uint32_le_as_int s (i + 12) in let cd_start = get_uint32_le_as_int s (i + 16) in if cd_start + cd_size > String.length s then error_eocd () else cd_start, cd_size, cd_member_count in (* eocd ends with a variable length comment, we need to search to find it. Good luck if the comment contains the sig… *) let rec loop min_start start = if start < min_start || start < 0 then error_no_eocd () else if String.get_int32_le s start = eocd_sig then info_of_eocd s start else loop min_start (start - 1) in let len = String.length s in let max_comment_size = 65535 in let min_start = len - max_comment_size - eocd_min_size in let start = len - eocd_min_size in if start < 0 then error_short () else loop min_start start let string_has_magic s = if String.length s < 4 then false else let m = String.get_int32_le s 0 in Int32.equal m lfh_sig || Int32.equal m eocd_sig let of_binary_string s = try let cd_start, cd_size, count = find_cd_info_in_eocd s in let cd_max = cd_start + cd_size - 1 in Ok (decode_cd_members empty count s cd_max cd_start) with | Failure e -> Error e (* Encoding *) let cleaned_gp_flags file = (* Bit 3 indicates presence of data descriptors which we never encode. So we make sure the bit is cleared. *) File.gp_flags file land (lnot (1 lsl 3)) let encoding_size z = let add_member m acc = let path_len = String.length (Member.path m) in let data_size = match Member.kind m with | Dir -> 0 | File f -> File.compressed_size f in acc + lfh_min_size + path_len + data_size + cdfh_min_size + path_len in fold add_member z eocd_min_size let encode_member b m (start, acc) = let encode_dir_lfh b start = Bytes.set_uint16_le b (start + 4) File.version_needed_to_extract_default; Bytes.set_uint16_le b (start + 6) File.gp_default; Bytes.set_uint16_le b (start + 8) (compression_to_int Stored); Bytes.set_int32_le b (start + 14) 0l (* CRC *); Bytes.set_int32_le b (start + 18) 0l (* Compressed size *); Bytes.set_int32_le b (start + 22) 0l (* Uncompressed size *) in let encode_file_lfh b file start = Bytes.set_uint16_le b (start + 4) (File.version_needed_to_extract file); Bytes.set_uint16_le b (start + 6) (cleaned_gp_flags file); Bytes.set_uint16_le b (start + 8) (compression_to_int (File.compression file)); Bytes.set_int32_le b (start + 14) (File.decompressed_crc_32 file); Bytes.set_int32_le b (start + 18) (Int32.of_int (File.compressed_size file)); Bytes.set_int32_le b (start + 22) (Int32.of_int (File.decompressed_size file)) in let path = Member.path m in let path_length = String.length path in let date, time = Ptime.to_dos_date_time (Member.mtime m) in Bytes.set_int32_le b (start ) lfh_sig; Bytes.set_uint16_le b (start + 10) time; Bytes.set_uint16_le b (start + 12) date; Bytes.set_uint16_le b (start + 26) path_length; Bytes.set_uint16_le b (start + 28) 0; (* Extra field length *) Bytes.blit_string path 0 b (start + 30) path_length; let next = match Member.kind m with | Dir -> encode_dir_lfh b start; start + 30 + path_length | File file -> encode_file_lfh b file start; let start = start + 30 + path_length in let src = File.compressed_bytes file and src_start = File.start file in let size = File.compressed_size file in Bytes.blit_string src src_start b start size; start + size in (next, (start, m) :: acc) let encode_cd_member b start (lfh_offset, m) = let encode_dir_cd_member b start = Bytes.set_uint16_le b (start + 4) File.version_made_by_default; Bytes.set_uint16_le b (start + 6) File.version_needed_to_extract_default; Bytes.set_uint16_le b (start + 8) File.gp_default; Bytes.set_uint16_le b (start + 10) (compression_to_int Stored); Bytes.set_int32_le b (start + 16) 0l (* CRC *); Bytes.set_int32_le b (start + 20) 0l (* Compressed size *); Bytes.set_int32_le b (start + 24) 0l (* Uncompressed size *) in let encode_file_cd_member b file start = Bytes.set_uint16_le b (start + 4) (File.version_made_by file); Bytes.set_uint16_le b (start + 6) (File.version_needed_to_extract file); Bytes.set_uint16_le b (start + 8) (cleaned_gp_flags file); Bytes.set_uint16_le b (start + 10) (compression_to_int (File.compression file)); Bytes.set_int32_le b (start + 16) (File.decompressed_crc_32 file); Bytes.set_int32_le b (start + 20) (Int32.of_int (File.compressed_size file)); Bytes.set_int32_le b (start + 24) (Int32.of_int (File.decompressed_size file)) in let path = Member.path m in let path_length = String.length path in let date, time = Ptime.to_dos_date_time (Member.mtime m) in let extatts_hi, extatts_lo = let kind_hi, kind_lo = match Member.kind m with | Dir -> 0o040000, 0x10 (* MS-DOS dir bit *) | File _ -> 0o100000, 0 in kind_hi lor (Member.mode m land 0o7777), kind_lo in Bytes.set_int32_le b (start ) cdfh_sig; Bytes.set_uint16_le b (start + 12) time; Bytes.set_uint16_le b (start + 14) date; Bytes.set_uint16_le b (start + 28) path_length; Bytes.set_uint16_le b (start + 30) 0; (* Extra field length *) Bytes.set_uint16_le b (start + 32) 0; (* File comment length *) Bytes.set_uint16_le b (start + 34) 0; (* Disk number start *) Bytes.set_uint16_le b (start + 36) 0; (* Internal file attributes *) Bytes.set_uint16_le b (start + 38) extatts_lo; Bytes.set_uint16_le b (start + 40) extatts_hi; Bytes.set_int32_le b (start + 42) (Int32.of_int lfh_offset); Bytes.blit_string path 0 b (start + 46) path_length; begin match Member.kind m with | Dir -> encode_dir_cd_member b start | File file -> encode_file_cd_member b file start end; start + 46 + path_length let err_uint32 kind n = Error (strf "Maximum ZIP %s 4294967295 exceeded (%d)" kind n) let err_cd_start n = err_uint32 "central directory offset" n let err_cd_size n = err_uint32 "central directory size" n let encode_eocd b start ~member_count ~cd_start ~cd_size = if cd_start > uint32_max_or_max_int then err_cd_start cd_start else if cd_size > uint32_max_or_max_int then err_cd_size cd_size else begin Bytes.set_int32_le b (start ) eocd_sig; Bytes.set_uint16_le b (start + 4) 0; (* Number of this disk *) Bytes.set_uint16_le b (start + 6) 0; (* Disk where cd starts *) Bytes.set_uint16_le b (start + 8) member_count; Bytes.set_uint16_le b (start + 10) member_count; Bytes.set_int32_le b (start + 12) (Int32.of_int cd_size); Bytes.set_int32_le b (start + 16) (Int32.of_int cd_start); Bytes.set_uint16_le b (start + 20) 0; (* Comment length *) Ok () end let default_first = "mimetype" let write_bytes ?(first = default_first) z ?(start = 0) b = if is_empty z then encode_eocd b start ~member_count:0 ~cd_start:0 ~cd_size:0 else let count = member_count z in if count > Member.max then Error (Member.err_count count) else let cd_start, ms = match String_map.find_opt first z with | None -> fold (encode_member b) z (start, []) | Some m -> let acc = encode_member b m (start, []) in fold (encode_member b) (remove first z) acc in let eocd_start = List.fold_left (encode_cd_member b) cd_start (List.rev ms) in let cd_size = eocd_start - cd_start in encode_eocd b eocd_start ~member_count:count ~cd_start ~cd_size let to_binary_string ?first z = let b = Bytes.create (encoding_size z) in let* () = write_bytes ?first z b in Ok (Bytes.unsafe_to_string b)