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)]; }