-
Notifications
You must be signed in to change notification settings - Fork 270
/
compress.ts
252 lines (243 loc) · 8.75 KB
/
compress.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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import {
COMPRESSION_DEFLATE,
COMPRESSION_NONE,
CentralDirectoryEndEntry,
CentralDirectoryEntry,
FileHeader,
} from './common';
import {
SIGNATURE_CENTRAL_DIRECTORY_END,
SIGNATURE_CENTRAL_DIRECTORY,
SIGNATURE_FILE,
} from './common';
import { iteratorToStream } from '../utils/iterator-to-stream';
import { collectBytes } from '../utils/collect-bytes';
import { crc32 } from './crc32';
/**
* Compresses the given files into a ZIP archive.
*
* @param files - An async or sync iterable of files to be compressed.
* @returns A readable stream of the compressed ZIP archive as Uint8Array chunks.
*/
export function zipFiles(
files: AsyncIterable<File> | Iterable<File>
): ReadableStream<Uint8Array> {
return iteratorToStream(files).pipeThrough(encodeZip());
}
/**
* Encodes the files into a ZIP format.
*
* @returns A stream transforming File objects into zipped bytes.
*/
function encodeZip() {
const offsetToFileHeaderMap: Map<number, FileHeader> = new Map();
let writtenBytes = 0;
return new TransformStream<File, Uint8Array>({
async transform(file, controller) {
const entryBytes = new Uint8Array(await file.arrayBuffer());
const compressed = (await collectBytes(
new Blob([entryBytes])
.stream()
.pipeThrough(new CompressionStream('deflate-raw'))
))!;
const crcHash = crc32(entryBytes);
const encodedPath = new TextEncoder().encode(file.name);
const zipFileEntry: FileHeader = {
signature: SIGNATURE_FILE,
version: 2,
generalPurpose: 0,
compressionMethod:
file.type === 'directory' || compressed.byteLength === 0
? COMPRESSION_NONE
: COMPRESSION_DEFLATE,
lastModifiedTime: 0,
lastModifiedDate: 0,
crc: crcHash,
compressedSize: compressed.byteLength,
uncompressedSize: entryBytes.byteLength,
path: encodedPath,
extra: new Uint8Array(0),
};
offsetToFileHeaderMap.set(writtenBytes, zipFileEntry);
const headerBytes = encodeFileEntryHeader(zipFileEntry);
controller.enqueue(headerBytes);
writtenBytes += headerBytes.byteLength;
controller.enqueue(compressed);
writtenBytes += compressed.byteLength;
},
flush(controller) {
const centralDirectoryOffset = writtenBytes;
let centralDirectorySize = 0;
for (const [
fileOffset,
header,
] of offsetToFileHeaderMap.entries()) {
const centralDirectoryEntry: Partial<CentralDirectoryEntry> = {
...header,
signature: SIGNATURE_CENTRAL_DIRECTORY,
fileComment: new Uint8Array(0),
diskNumber: 1,
internalAttributes: 0,
externalAttributes: 0,
firstByteAt: fileOffset,
};
const centralDirectoryEntryBytes = encodeCentralDirectoryEntry(
centralDirectoryEntry as CentralDirectoryEntry,
fileOffset
);
controller.enqueue(centralDirectoryEntryBytes);
centralDirectorySize += centralDirectoryEntryBytes.byteLength;
}
const centralDirectoryEnd: CentralDirectoryEndEntry = {
signature: SIGNATURE_CENTRAL_DIRECTORY_END,
numberOfDisks: 1,
centralDirectoryOffset,
centralDirectorySize,
centralDirectoryStartDisk: 1,
numberCentralDirectoryRecordsOnThisDisk:
offsetToFileHeaderMap.size,
numberCentralDirectoryRecords: offsetToFileHeaderMap.size,
comment: new Uint8Array(0),
};
const centralDirectoryEndBytes =
encodeCentralDirectoryEnd(centralDirectoryEnd);
controller.enqueue(centralDirectoryEndBytes);
offsetToFileHeaderMap.clear();
},
});
}
/**
* Encodes a file entry header as a Uint8Array.
*
* The array is structured as follows:
*
* ```
* Offset Bytes Description
* 0 4 Local file header signature = 0x04034b50 (PK♥♦ or "PK\3\4")
* 4 2 Version needed to extract (minimum)
* 6 2 General purpose bit flag
* 8 2 Compression method; e.g. none = 0, DEFLATE = 8 (or "\0x08\0x00")
* 10 2 File last modification time
* 12 2 File last modification date
* 14 4 CRC-32 of uncompressed data
* 18 4 Compressed size (or 0xffffffff for ZIP64)
* 22 4 Uncompressed size (or 0xffffffff for ZIP64)
* 26 2 File name length (n)
* 28 2 Extra field length (m)
* 30 n File name
* 30+n m Extra field
* ```
*/
function encodeFileEntryHeader(entry: FileHeader) {
const buffer = new ArrayBuffer(
30 + entry.path.byteLength + entry.extra.byteLength
);
const view = new DataView(buffer);
view.setUint32(0, entry.signature, true);
view.setUint16(4, entry.version, true);
view.setUint16(6, entry.generalPurpose, true);
view.setUint16(8, entry.compressionMethod, true);
view.setUint16(10, entry.lastModifiedDate, true);
view.setUint16(12, entry.lastModifiedTime, true);
view.setUint32(14, entry.crc, true);
view.setUint32(18, entry.compressedSize, true);
view.setUint32(22, entry.uncompressedSize, true);
view.setUint16(26, entry.path.byteLength, true);
view.setUint16(28, entry.extra.byteLength, true);
const uint8Header = new Uint8Array(buffer);
uint8Header.set(entry.path, 30);
uint8Header.set(entry.extra, 30 + entry.path.byteLength);
return uint8Header;
}
/**
* Encodes a central directory entry as a Uint8Array.
*
* The central directory entry is structured as follows:
*
* ```
* Offset Bytes Description
* 0 4 Central directory file header signature = 0x02014b50
* 4 2 Version made by
* 6 2 Version needed to extract (minimum)
* 8 2 General purpose bit flag
* 10 2 Compression method
* 12 2 File last modification time
* 14 2 File last modification date
* 16 4 CRC-32 of uncompressed data
* 20 4 Compressed size (or 0xffffffff for ZIP64)
* 24 4 Uncompressed size (or 0xffffffff for ZIP64)
* 28 2 File name length (n)
* 30 2 Extra field length (m)
* 32 2 File comment length (k)
* 34 2 Disk number where file starts (or 0xffff for ZIP64)
* 36 2 Internal file attributes
* 38 4 External file attributes
* 42 4 Relative offset of local file header (or 0xffffffff for ZIP64). This is the number of bytes between the start of the first disk on which the file occurs, and the start of the local file header. This allows software reading the central directory to locate the position of the file inside the ZIP file.
* 46 n File name
* 46+n m Extra field
* 46+n+m k File comment
* ```
*/
function encodeCentralDirectoryEntry(
entry: CentralDirectoryEntry,
fileEntryOffset: number
) {
const buffer = new ArrayBuffer(
46 + entry.path.byteLength + entry.extra.byteLength
);
const view = new DataView(buffer);
view.setUint32(0, entry.signature, true);
view.setUint16(4, entry.versionCreated, true);
view.setUint16(6, entry.versionNeeded, true);
view.setUint16(8, entry.generalPurpose, true);
view.setUint16(10, entry.compressionMethod, true);
view.setUint16(12, entry.lastModifiedDate, true);
view.setUint16(14, entry.lastModifiedTime, true);
view.setUint32(16, entry.crc, true);
view.setUint32(20, entry.compressedSize, true);
view.setUint32(24, entry.uncompressedSize, true);
view.setUint16(28, entry.path.byteLength, true);
view.setUint16(30, entry.extra.byteLength, true);
view.setUint16(32, entry.fileComment.byteLength, true);
view.setUint16(34, entry.diskNumber, true);
view.setUint16(36, entry.internalAttributes, true);
view.setUint32(38, entry.externalAttributes, true);
view.setUint32(42, fileEntryOffset, true);
const uint8Header = new Uint8Array(buffer);
uint8Header.set(entry.path, 46);
uint8Header.set(entry.extra, 46 + entry.path.byteLength);
return uint8Header;
}
/**
* Encodes the end of central directory entry as a Uint8Array.
*
* The end of central directory entry is structured as follows:
*
* ```
* Offset Bytes Description[33]
* 0 4 End of central directory signature = 0x06054b50
* 4 2 Number of this disk (or 0xffff for ZIP64)
* 6 2 Disk where central directory starts (or 0xffff for ZIP64)
* 8 2 Number of central directory records on this disk (or 0xffff for ZIP64)
* 10 2 Total number of central directory records (or 0xffff for ZIP64)
* 12 4 Size of central directory (bytes) (or 0xffffffff for ZIP64)
* 16 4 Offset of start of central directory, relative to start of archive (or 0xffffffff for ZIP64)
* 20 2 Comment length (n)
* 22 n Comment
* ```
*/
function encodeCentralDirectoryEnd(entry: CentralDirectoryEndEntry) {
const buffer = new ArrayBuffer(22 + entry.comment.byteLength);
const view = new DataView(buffer);
view.setUint32(0, entry.signature, true);
view.setUint16(4, entry.numberOfDisks, true);
view.setUint16(6, entry.centralDirectoryStartDisk, true);
view.setUint16(8, entry.numberCentralDirectoryRecordsOnThisDisk, true);
view.setUint16(10, entry.numberCentralDirectoryRecords, true);
view.setUint32(12, entry.centralDirectorySize, true);
view.setUint32(16, entry.centralDirectoryOffset, true);
view.setUint16(20, entry.comment.byteLength, true);
const uint8Header = new Uint8Array(buffer);
uint8Header.set(entry.comment, 22);
return uint8Header;
}