Skip to content

Instantly share code, notes, and snippets.

@taralx
Created May 9, 2021 06:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save taralx/ec4ac1cbdd6cac8713298dec3f0dc8a9 to your computer and use it in GitHub Desktop.
Save taralx/ec4ac1cbdd6cac8713298dec3f0dc8a9 to your computer and use it in GitHub Desktop.
Zip files on web platform
const GZIP_HEADER = Uint8Array.from([
31, 139, // gzip magic
8, // deflate
0, // no extra fields
0, 0, 0, 0, // mtime (n/a)
0, 0, // extra flags, OS
]);
class ZipEntry {
constructor(f, meta) {
this.f = f;
this.meta = meta;
}
get size() {
return this.meta.uncompressed;
}
fetch() {
return new Response(this.stream());
}
stream() {
let s = this.f.slice(this.meta.offset, this.meta.offset+this.meta.compressed).stream()
if (this.meta.method === 8) {
// deflate uses adler32, but gzip uses the same crc32 as zip!
s = s.pipeThrough(new TransformStream({
meta: this.meta,
start(controller) {
controller.enqueue(GZIP_HEADER);
},
flush(controller) {
const tmp = new DataView(new ArrayBuffer(8));
tmp.setUint32(0, this.meta.crc, true);
tmp.setUint32(4, this.meta.uncompressed, true);
controller.enqueue(new Uint8Array(tmp.buffer));
}
}));
s = s.pipeThrough(new DecompressionStream('gzip'));
}
return s;
}
}
export async function openZip(f) {
let eocd;
const b = new DataView(await f.slice(-(22+65535)).arrayBuffer());
for (let i = b.byteLength - 22; i >= 0; i--) {
if (b.getUint32(i, true) == 0x06054b50 && i + 22 + b.getUint16(i+20, true) <= b.byteLength) {
eocd = new DataView(b.buffer.slice(i));
break;
}
}
if (eocd == null) {
throw new Error('Unable to locate end of central directory')
}
const nrecs = eocd.getUint16(10, true);
if (eocd.getUint16(4, true) !== 0 || eocd.getUint16(6, true) !== 0 || eocd.getUint16(8, true) !== nrecs) {
throw new Error('Split archives not supported');
}
const cdlen = eocd.getUint32(12, true);
const cdstart = f.size - eocd.byteLength - cdlen;
const pfx = cdstart - eocd.getUint32(16, true);
if (pfx < 0) {
throw new Error('ZIP file corrupted: CD length does not match up')
}
const cd = new DataView(await f.slice(cdstart, cdstart+cdlen).arrayBuffer());
const files = new Map;
const decoder = new TextDecoder();
for (let i = 0; i < cdlen;) {
if (cd.getUint32(i, true) !== 0x02014b50) {
throw new Error('Central directory corrupted');
}
const fnlen = cd.getUint16(i+28, true);
const eflen = cd.getUint16(i+30, true);
const cmtlen = cd.getUint16(i+32, true);
const nexti = i + 46 + fnlen + eflen + cmtlen;
if (nexti > cdlen) {
throw new Error('Central directory corrupted');
}
const minver = cd.getUint8(i+6);
const flags = cd.getUint8(i+8);
const encrypted = flags & 1 !== 0;
const delayedSizes = flags & 8 !== 0;
const method = cd.getUint16(i+10, true);
const disk = cd.getUint16(i+34, true);
if (minver <= 20 && (method === 0 || method === 8) && !encrypted && !delayedSizes && disk === 0) {
const fn = decoder.decode(new Uint8Array(cd.buffer, i+46, fnlen));
if (!fn.endsWith('/')) {
files.set(fn, new ZipEntry(f, {
method: method,
offset: pfx+cd.getUint32(i+42, true)+30+fnlen+eflen,
compressed: cd.getUint32(i+20, true),
uncompressed: cd.getUint32(i+24, true),
crc: cd.getUint32(i+16, true),
}));
}
}
i = nexti;
}
return files;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment