import crc32 from 'crc/crc32'; const encoder = new TextEncoder(); function dosDateTime(dateTime = new Date()) { const year = (dateTime.getFullYear() - 1980) << 9; const month = (dateTime.getMonth() + 1) << 5; const day = dateTime.getDate(); const date = year | month | day; const hour = dateTime.getHours() << 11; const minute = dateTime.getMinutes() << 5; const second = Math.floor(dateTime.getSeconds() / 2); const time = hour | minute | second; return { date, time }; } class File { constructor(info) { this.name = encoder.encode(info.name); this.size = info.size; this.bytesRead = 0; this.crc = null; this.dateTime = dosDateTime(); } get header() { const h = new ArrayBuffer(30 + this.name.byteLength); const v = new DataView(h); v.setUint32(0, 0x04034b50, true); // sig v.setUint16(4, 20, true); // version v.setUint16(6, 8, true); // bit flags (8 = use data descriptor) v.setUint16(8, 0, true); // compression v.setUint16(10, this.dateTime.time, true); // modified time v.setUint16(12, this.dateTime.date, true); // modified date v.setUint32(14, 0, true); // crc32 (in descriptor) v.setUint32(18, 0, true); // compressed size (in descriptor) v.setUint32(22, 0, true); // uncompressed size (in descriptor) v.setUint16(26, this.name.byteLength, true); // name length v.setUint16(28, 0, true); // extra field length for (let i = 0; i < this.name.byteLength; i++) { v.setUint8(30 + i, this.name[i]); } return new Uint8Array(h); } get dataDescriptor() { const dd = new ArrayBuffer(16); const v = new DataView(dd); v.setUint32(0, 0x08074b50, true); // sig v.setUint32(4, this.crc, true); // crc32 v.setUint32(8, this.size, true); // compressed size v.setUint32(12, this.size, true); // uncompressed size return new Uint8Array(dd); } directoryRecord(offset) { const dr = new ArrayBuffer(46 + this.name.byteLength); const v = new DataView(dr); v.setUint32(0, 0x02014b50, true); // sig v.setUint16(4, 20, true); // version made v.setUint16(6, 20, true); // version required v.setUint16(8, 8, true); // bit flags (8 = use data descriptor) v.setUint16(10, 0, true); // compression v.setUint16(12, this.dateTime.time, true); // modified time v.setUint16(14, this.dateTime.date, true); // modified date v.setUint32(16, this.crc, true); // crc v.setUint32(20, this.size, true); // compressed size v.setUint32(24, this.size, true); // uncompressed size v.setUint16(28, this.name.byteLength, true); // name length v.setUint16(30, 0, true); // extra length v.setUint16(32, 0, true); // comment length v.setUint16(34, 0, true); // disk number v.setUint16(36, 0, true); // internal file attrs v.setUint32(38, 0, true); // external file attrs v.setUint32(42, offset, true); // file offset for (let i = 0; i < this.name.byteLength; i++) { v.setUint8(46 + i, this.name[i]); } return new Uint8Array(dr); } get byteLength() { return this.size + this.name.byteLength + 30 + 16; } append(data, controller) { this.bytesRead += data.byteLength; const endIndex = data.byteLength - Math.max(this.bytesRead - this.size, 0); const buf = data.slice(0, endIndex); this.crc = crc32(buf, this.crc); controller.enqueue(buf); if (endIndex < data.byteLength) { return data.slice(endIndex, data.byteLength); } } } function centralDirectory(files, controller) { let directoryOffset = 0; let directorySize = 0; for (let i = 0; i < files.length; i++) { const file = files[i]; const record = file.directoryRecord(directoryOffset); directoryOffset += file.byteLength; controller.enqueue(record); directorySize += record.byteLength; } controller.enqueue(eod(files.length, directorySize, directoryOffset)); } function eod(fileCount, directorySize, directoryOffset) { const e = new ArrayBuffer(22); const v = new DataView(e); v.setUint32(0, 0x06054b50, true); // sig v.setUint16(4, 0, true); // disk number v.setUint16(6, 0, true); // directory disk v.setUint16(8, fileCount, true); // number of records v.setUint16(10, fileCount, true); // total records v.setUint32(12, directorySize, true); // size of directory v.setUint32(16, directoryOffset, true); // offset of directory v.setUint16(20, 0, true); // comment length return new Uint8Array(e); } class ZipStreamController { constructor(files, source) { this.files = files; this.fileIndex = 0; this.file = null; this.reader = source.getReader(); this.nextFile(); this.extra = null; } nextFile() { this.file = this.files[this.fileIndex++]; } async pull(controller) { if (!this.file) { // end of archive centralDirectory(this.files, controller); return controller.close(); } if (this.file.bytesRead === 0) { // beginning of file controller.enqueue(this.file.header); if (this.extra) { this.extra = this.file.append(this.extra, controller); } } if (this.file.bytesRead >= this.file.size) { // end of file controller.enqueue(this.file.dataDescriptor); this.nextFile(); return this.pull(controller); } const data = await this.reader.read(); if (data.done) { this.nextFile(); return this.pull(controller); } this.extra = this.file.append(data.value, controller); } } export default class Zip { constructor(manifest, source) { this.files = manifest.files.map(info => new File(info)); this.source = source; } get stream() { return new ReadableStream(new ZipStreamController(this.files, this.source)); } get size() { const entries = this.files.reduce( (total, file) => total + file.byteLength * 2 - file.size, 0 ); const eod = 22; return entries + eod; } }