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
|
//// 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
|