reilly-downloader/safaribooks/zip.js

123 lines
4.2 KiB
JavaScript

const CRC_TABLE = (() => {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
table[i] = c >>> 0;
}
return table;
})();
function crc32(data) {
let crc = 0xffffffff;
for (let i = 0; i < data.length; i++) {
const byte = data[i];
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
const textEncoder = new TextEncoder();
export class SimpleZip {
constructor() {
this.files = [];
}
addFile(path, data) {
if (!(data instanceof Uint8Array)) {
throw new TypeError(`File data for ${path} must be a Uint8Array.`);
}
const cleanPath = path.replace(/\\/g, "/");
const nameBytes = textEncoder.encode(cleanPath);
const checksum = crc32(data);
this.files.push({
name: cleanPath,
nameBytes,
data,
crc: checksum,
localHeaderOffset: 0
});
}
generate() {
const parts = [];
let offset = 0;
for (const file of this.files) {
const headerLength = 30 + file.nameBytes.length;
const headerBuffer = new ArrayBuffer(headerLength);
const headerView = new DataView(headerBuffer);
file.localHeaderOffset = offset;
headerView.setUint32(0, 0x04034b50, true);
headerView.setUint16(4, 20, true); // version needed to extract
headerView.setUint16(6, 0, true); // general purpose flag
headerView.setUint16(8, 0, true); // compression (store)
headerView.setUint16(10, 0, true); // mod time
headerView.setUint16(12, 0, true); // mod date
headerView.setUint32(14, file.crc, true);
headerView.setUint32(18, file.data.length, true);
headerView.setUint32(22, file.data.length, true);
headerView.setUint16(26, file.nameBytes.length, true);
headerView.setUint16(28, 0, true); // extra length
const headerBytes = new Uint8Array(headerBuffer);
headerBytes.set(file.nameBytes, 30);
parts.push(headerBytes, file.data);
offset += headerBytes.length + file.data.length;
}
const centralDirectoryOffset = offset;
let centralDirectorySize = 0;
for (const file of this.files) {
const centralLength = 46 + file.nameBytes.length;
const centralBuffer = new ArrayBuffer(centralLength);
const centralView = new DataView(centralBuffer);
centralView.setUint32(0, 0x02014b50, true);
centralView.setUint16(4, 0x0014, true); // version made by
centralView.setUint16(6, 20, true); // version needed to extract
centralView.setUint16(8, 0, true); // general purpose flag
centralView.setUint16(10, 0, true); // compression
centralView.setUint16(12, 0, true); // mod time
centralView.setUint16(14, 0, true); // mod date
centralView.setUint32(16, file.crc, true);
centralView.setUint32(20, file.data.length, true);
centralView.setUint32(24, file.data.length, true);
centralView.setUint16(28, file.nameBytes.length, true);
centralView.setUint16(30, 0, true); // extra field length
centralView.setUint16(32, 0, true); // comment length
centralView.setUint16(34, 0, true); // disk number start
centralView.setUint16(36, 0, true); // internal attrs
centralView.setUint32(38, 0, true); // external attrs
centralView.setUint32(42, file.localHeaderOffset, true);
const centralBytes = new Uint8Array(centralBuffer);
centralBytes.set(file.nameBytes, 46);
parts.push(centralBytes);
centralDirectorySize += centralBytes.length;
offset += centralBytes.length;
}
const endBuffer = new ArrayBuffer(22);
const endView = new DataView(endBuffer);
endView.setUint32(0, 0x06054b50, true);
endView.setUint16(4, 0, true); // number of this disk
endView.setUint16(6, 0, true); // disk where central directory starts
endView.setUint16(8, this.files.length, true); // records on this disk
endView.setUint16(10, this.files.length, true); // total records
endView.setUint32(12, centralDirectorySize, true);
endView.setUint32(16, centralDirectoryOffset, true);
endView.setUint16(20, 0, true); // comment length
parts.push(new Uint8Array(endBuffer));
return new Blob(parts, { type: "application/epub+zip" });
}
}