summaryrefslogtreecommitdiffhomepage
path: root/gleam/src/write/zip.gleam
diff options
context:
space:
mode:
Diffstat (limited to 'gleam/src/write/zip.gleam')
-rw-r--r--gleam/src/write/zip.gleam132
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