diff options
Diffstat (limited to 'gleam/src/write/zip.gleam')
| -rw-r--r-- | gleam/src/write/zip.gleam | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/gleam/src/write/zip.gleam b/gleam/src/write/zip.gleam new file mode 100644 index 0000000..05cb56a --- /dev/null +++ b/gleam/src/write/zip.gleam @@ -0,0 +1,132 @@ +//// Writing a zip, stored only. +//// +//// An epub is a zip with rules: the first entry must be "mimetype", stored, +//// with no extra field, so that a reading system can identify the file from +//// its first bytes without inflating anything. Everything else may be +//// deflated and does not have to be. +//// +//// So: stored only. Deflate would buy compression on a sample writer's +//// output, which is not what is being demonstrated, and it would put a second +//// thing in the way of the two writers agreeing byte for byte. +//// +//// No timestamps. Every entry is dated 1980-01-01, the epoch of the format, +//// which is what makes the same document produce the same bytes twice. + +import gleam/bit_array +import gleam/list + +pub type Entry { + Entry(name: String, data: BitArray) +} + +const method_stored = 0 + +/// 2.0, which stored requires +const version_needed = 20 + +const dos_time = 0 + +/// 1980-01-01 +const dos_date = 0x0021 + +/// The archive, entries in the order given. +pub fn write(entries: List(Entry)) -> BitArray { + let #(local, offsets, crcs, _) = + list.fold(entries, #(<<>>, [], [], 0), fn(state, e) { + let #(acc, offsets, crcs, at) = state + let crc = crc32(e.data) + let header = local_header(e, crc) + let size = bit_array.byte_size(header) + bit_array.byte_size(e.data) + #( + bit_array.concat([acc, header, e.data]), + [at, ..offsets], + [crc, ..crcs], + at + size, + ) + }) + let offsets = list.reverse(offsets) + let crcs = list.reverse(crcs) + let cd_offset = bit_array.byte_size(local) + let central = + list.zip(entries, list.zip(crcs, offsets)) + |> list.map(fn(t) { + let #(e, #(crc, offset)) = t + central_entry(e, crc, offset) + }) + |> bit_array.concat + let cd_size = bit_array.byte_size(central) + bit_array.concat([ + local, + central, + end_record(list.length(entries), cd_size, cd_offset), + ]) +} + +fn local_header(e: Entry, crc: Int) -> BitArray { + let name = <<e.name:utf8>> + let size = bit_array.byte_size(e.data) + bit_array.concat([ + <<0x04034b50:little-size(32)>>, + <<version_needed:little-size(16)>>, + // flags: none; the name is ascii + <<0:little-size(16)>>, + <<method_stored:little-size(16)>>, + <<dos_time:little-size(16)>>, + <<dos_date:little-size(16)>>, + <<crc:little-size(32)>>, + <<size:little-size(32)>>, + <<size:little-size(32)>>, + <<bit_array.byte_size(name):little-size(16)>>, + // no extra field, which "mimetype" requires + <<0:little-size(16)>>, + name, + ]) +} + +fn central_entry(e: Entry, crc: Int, offset: Int) -> BitArray { + let name = <<e.name:utf8>> + let size = bit_array.byte_size(e.data) + bit_array.concat([ + <<0x02014b50:little-size(32)>>, + // made by + <<version_needed:little-size(16)>>, + <<version_needed:little-size(16)>>, + <<0:little-size(16)>>, + <<method_stored:little-size(16)>>, + <<dos_time:little-size(16)>>, + <<dos_date:little-size(16)>>, + <<crc:little-size(32)>>, + <<size:little-size(32)>>, + <<size:little-size(32)>>, + <<bit_array.byte_size(name):little-size(16)>>, + // extra, comment, disk, internal attributes + <<0:little-size(16)>>, + <<0:little-size(16)>>, + <<0:little-size(16)>>, + <<0:little-size(16)>>, + // external attributes + <<0:little-size(32)>>, + <<offset:little-size(32)>>, + name, + ]) +} + +fn end_record(count: Int, cd_size: Int, cd_offset: Int) -> BitArray { + bit_array.concat([ + <<0x06054b50:little-size(32)>>, + // this disk, and the disk the central directory starts on + <<0:little-size(16)>>, + <<0:little-size(16)>>, + <<count:little-size(16)>>, + <<count:little-size(16)>>, + <<cd_size:little-size(32)>>, + <<cd_offset:little-size(32)>>, + // no archive comment + <<0:little-size(16)>>, + ]) +} + +/// Erlang has crc32 in the zlib module, which is built in: the NIF is for +/// compression, and the checksum is not. +@external(erlang, "erlang", "crc32") +fn crc32(data: BitArray) -> Int |
