-
Notifications
You must be signed in to change notification settings - Fork 0
/
zip.ts
98 lines (89 loc) · 3.89 KB
/
zip.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
const OS_UNIX = 3;
export class ZipFile {
name: string;
fileOffset: number;
compressedSize: number;
uncompressedSize: number;
crc32: number;
method: number;
gpbf: number;
isEncrypted(): boolean {
return (this.gpbf & 1) !== 0;
}
constructor(private file: Blob, cde: Uint8Array) {
const v = new DataView(cde.buffer, cde.byteOffset, cde.byteLength);
if (v.getUint32(0, true) !== 0x02014B50) { // "PK\001\002"
throw new Error('Invalid central directory');
}
const versionMadeBy = v.getUint16(4, true);
const versionNeeded = v.getUint16(6, true);
if (versionNeeded > 20) throw new Error('Unsupported ZIP version: ' + versionNeeded);
this.gpbf = v.getUint16(8, true);
this.method = v.getUint16(10, true);
this.crc32 = v.getUint32(16, true);
this.compressedSize = v.getUint32(20, true);
this.uncompressedSize = v.getUint32(24, true);
const fileNameLength = v.getUint16(28, true);
this.fileOffset = v.getUint32(42, true);
const encoding = this.guessPathEncoding(versionMadeBy);
this.name = new TextDecoder(encoding, { fatal: true }).decode(cde.subarray(46, 46 + fileNameLength));
}
guessPathEncoding(versionMadeBy: number): string {
if (this.gpbf & 0x800) return 'utf-8';
const os = versionMadeBy >> 8;
if (os === OS_UNIX) return 'utf-8';
return 'shift_jis';
}
async compressedData(): Promise<Blob> {
const localHeader = await readBytes(this.file, this.fileOffset, 30);
const lhView = new DataView(localHeader);
if (lhView.getUint32(0, true) !== 0x04034B50) { // "PK\003\004"
throw new Error('Invalid local header');
}
const compressedDataOffset = this.fileOffset + 30 + lhView.getUint16(26, true) + lhView.getUint16(28, true);
return this.file.slice(compressedDataOffset, compressedDataOffset + this.compressedSize);
}
async extract(): Promise<Uint8Array> {
if (this.isEncrypted()) throw new Error('Encrypted ZIP files are not supported');
if (this.method === 0) {
return new Uint8Array(await (await this.compressedData()).arrayBuffer());
}
if (this.method !== 8) throw new Error('Unsupported compression method: ' + this.method);
const stream = (await this.compressedData()).stream().pipeThrough(new DecompressionStream('deflate-raw'));
const data = await new Response(stream).arrayBuffer();
// TODO: Verify CRC32.
return new Uint8Array(data);
}
}
export async function load(file: Blob): Promise<ZipFile[]> {
// Find the OECD record.
const oecdBuf = await readBytes(file, Math.max(0, file.size - 65558), Math.min(65558, file.size));
const view = new DataView(oecdBuf);
let oecdp = oecdBuf.byteLength - 22;
while (oecdp >= 0) {
if (view.getUint32(oecdp, true) === 0x06054B50) { // "PK\005\006"
break;
}
oecdp--;
}
if (oecdp < 0) throw new Error('Not a ZIP file');
// Read the central directory.
const cdSize = view.getUint32(oecdp + 12, true);
const cdOffset = view.getUint32(oecdp + 16, true);
const cdBuf = await readBytes(file, cdOffset, cdSize);
const cdView = new DataView(cdBuf);
let pos = 0;
const files: ZipFile[] = [];
while (pos < cdSize) {
const fileNameLength = cdView.getUint16(pos + 28, true);
const extraFieldLength = cdView.getUint16(pos + 30, true);
const commentLength = cdView.getUint16(pos + 32, true);
const cdeSize = 46 + fileNameLength + extraFieldLength + commentLength;
files.push(new ZipFile(file, new Uint8Array(cdBuf, pos, cdeSize)));
pos += cdeSize;
}
return files;
}
function readBytes(file: Blob, offset: number, length: number): Promise<ArrayBuffer> {
return file.slice(offset, offset + length).arrayBuffer();
}