summaryrefslogtreecommitdiffhomepage
path: root/dlang/src/write/zip.d
blob: cfd8419f6fbd96cb9ed2fff654792f102ab8840e (plain)
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
module write.zip;

/+ ↓ 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 need zlib and 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. What is written here is a valid zip that epubcheck accepts.
     .
     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. Spine
     does the same.
+/

import std.array : appender;
import std.digest.crc : crc32Of;
import std.string : representation;

struct ZipEntryOut {
  string  name;
  ubyte[] data;
}

/+ ↓ the archive, entries in the order given +/
ubyte[] zipWrite(ZipEntryOut[] entries) {
  auto out_ = appender!(ubyte[]);
  uint[] offsets;
  uint[] crcs;
  foreach (e; entries) {
    offsets ~= cast(uint) out_.data.length;
    uint crc = crc32(e.data);
    crcs ~= crc;
    out_ ~= localHeader(e, crc);
    out_ ~= e.data;
  }
  uint cd_offset = cast(uint) out_.data.length;
  foreach (i, e; entries) {
    out_ ~= centralEntry(e, crcs[i], offsets[i]);
  }
  uint cd_size = cast(uint) out_.data.length - cd_offset;
  out_ ~= endRecord(cast(ushort) entries.length, cd_size, cd_offset);
  return out_.data;
}

private enum ushort method_stored = 0;
private enum ushort version_needed = 20;   /+ 2.0, which stored requires +/
private enum ushort dos_time = 0;          /+ 00:00:00 +/
private enum ushort dos_date = 0x0021;     /+ 1980-01-01 +/

private ubyte[] localHeader(ZipEntryOut e, uint crc) {
  auto b = appender!(ubyte[]);
  b ~= u32(0x04034b50);
  b ~= u16(version_needed);
  b ~= u16(0);                 /+ flags: none; the name is ascii +/
  b ~= u16(method_stored);
  b ~= u16(dos_time);
  b ~= u16(dos_date);
  b ~= u32(crc);
  b ~= u32(cast(uint) e.data.length);
  b ~= u32(cast(uint) e.data.length);
  b ~= u16(cast(ushort) e.name.length);
  b ~= u16(0);                 /+ no extra field, which "mimetype" requires +/
  b ~= cast(ubyte[]) e.name.dup.representation.dup;
  return b.data;
}

private ubyte[] centralEntry(ZipEntryOut e, uint crc, uint offset) {
  auto b = appender!(ubyte[]);
  b ~= u32(0x02014b50);
  b ~= u16(version_needed);    /+ made by +/
  b ~= u16(version_needed);
  b ~= u16(0);
  b ~= u16(method_stored);
  b ~= u16(dos_time);
  b ~= u16(dos_date);
  b ~= u32(crc);
  b ~= u32(cast(uint) e.data.length);
  b ~= u32(cast(uint) e.data.length);
  b ~= u16(cast(ushort) e.name.length);
  b ~= u16(0);                 /+ extra +/
  b ~= u16(0);                 /+ comment +/
  b ~= u16(0);                 /+ disk +/
  b ~= u16(0);                 /+ internal attributes +/
  b ~= u32(0);                 /+ external attributes +/
  b ~= u32(offset);
  b ~= cast(ubyte[]) e.name.dup.representation.dup;
  return b.data;
}

private ubyte[] endRecord(ushort count, uint cd_size, uint cd_offset) {
  auto b = appender!(ubyte[]);
  b ~= u32(0x06054b50);
  b ~= u16(0);                 /+ this disk +/
  b ~= u16(0);                 /+ the disk the central directory starts on +/
  b ~= u16(count);
  b ~= u16(count);
  b ~= u32(cd_size);
  b ~= u32(cd_offset);
  b ~= u16(0);                 /+ no archive comment +/
  return b.data;
}

private uint crc32(const(ubyte)[] data) {
  auto d = crc32Of(data);
  /+ ↓ crc32Of gives the four bytes little endian, which is the order the
       header wants them in, so they are read back in that order +/
  return (cast(uint) d[0])
    | (cast(uint) d[1] << 8)
    | (cast(uint) d[2] << 16)
    | (cast(uint) d[3] << 24);
}

private ubyte[] u16(ushort v) {
  return [cast(ubyte)(v & 0xff), cast(ubyte)((v >> 8) & 0xff)];
}

private ubyte[] u32(uint v) {
  return [cast(ubyte)(v & 0xff), cast(ubyte)((v >> 8) & 0xff),
          cast(ubyte)((v >> 16) & 0xff), cast(ubyte)((v >> 24) & 0xff)];
}