send/app/zip.js

187 lines
5.8 KiB
JavaScript
Raw Normal View History

2018-07-26 05:26:11 +00:00
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, 0x808, true); // bit flags (use data descriptor(8) + utf8-encoded(8 << 8))
2018-07-26 05:26:11 +00:00
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
2018-07-26 05:26:11 +00:00
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, 0x808, true); // bit flags (use data descriptor(8) + utf8-encoded(8 << 8))
2018-07-26 05:26:11 +00:00
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;
}
}