123 lines
4.2 KiB
JavaScript
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" });
|
|
}
|
|
}
|