Created
May 9, 2021 06:48
-
-
Save taralx/ec4ac1cbdd6cac8713298dec3f0dc8a9 to your computer and use it in GitHub Desktop.
Zip files on web platform
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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