@php-wasm/stream-compression 0.0.1 → 0.9.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +339 -0
- package/index.cjs +1 -0
- package/index.d.ts +143 -0
- package/index.js +546 -0
- package/package.json +36 -29
- package/{src/test/vitest-setup-file.ts → test/vitest-setup-file.d.ts} +0 -1
- package/utils/append-bytes.d.ts +7 -0
- package/utils/collect-bytes.d.ts +8 -0
- package/utils/collect-file.d.ts +8 -0
- package/utils/collect-string.d.ts +8 -0
- package/utils/concat-bytes.d.ts +9 -0
- package/utils/concat-string.d.ts +6 -0
- package/utils/concat-uint8-array.d.ts +7 -0
- package/utils/filter-stream.d.ts +7 -0
- package/utils/iterable-stream-polyfill.d.ts +1 -0
- package/utils/iterator-to-stream.d.ts +8 -0
- package/utils/limit-bytes.d.ts +8 -0
- package/utils/prepend-bytes.d.ts +7 -0
- package/utils/skip-first-bytes.d.ts +7 -0
- package/utils/skip-last-bytes.d.ts +7 -0
- package/utils/streamed-file.d.ts +39 -0
- package/zip/decode-remote-zip.d.ts +14 -0
- package/zip/decode-zip.d.ts +82 -0
- package/zip/encode-zip.d.ts +7 -0
- package/{src/zip/index.ts → zip/index.d.ts} +0 -2
- package/zip/types.d.ts +66 -0
- package/.eslintrc.json +0 -18
- package/project.json +0 -34
- package/src/index.ts +0 -7
- package/src/test/append-bytes.spec.ts +0 -25
- package/src/test/decode-zip.spec.ts +0 -22
- package/src/test/encode-zip.spec.ts +0 -47
- package/src/test/fixtures/hello-dolly.zip +0 -0
- package/src/test/prepend-bytes.spec.ts +0 -25
- package/src/test/skip-first-bytes.spec.ts +0 -41
- package/src/test/skip-last-bytes.spec.ts +0 -27
- package/src/utils/append-bytes.ts +0 -16
- package/src/utils/collect-bytes.ts +0 -24
- package/src/utils/collect-file.ts +0 -16
- package/src/utils/collect-string.ts +0 -25
- package/src/utils/concat-bytes.ts +0 -38
- package/src/utils/concat-string.ts +0 -17
- package/src/utils/concat-uint8-array.ts +0 -17
- package/src/utils/filter-stream.ts +0 -15
- package/src/utils/iterable-stream-polyfill.ts +0 -35
- package/src/utils/iterator-to-stream.ts +0 -39
- package/src/utils/limit-bytes.ts +0 -40
- package/src/utils/prepend-bytes.ts +0 -18
- package/src/utils/skip-first-bytes.ts +0 -21
- package/src/utils/skip-last-bytes.ts +0 -24
- package/src/utils/streamed-file.ts +0 -58
- package/src/zip/decode-remote-zip.ts +0 -409
- package/src/zip/decode-zip.ts +0 -349
- package/src/zip/encode-zip.ts +0 -278
- package/src/zip/types.ts +0 -76
- package/tsconfig.json +0 -23
- package/tsconfig.lib.json +0 -14
- package/tsconfig.spec.json +0 -25
- package/vite.config.ts +0 -55
package/src/zip/decode-zip.ts
DELETED
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reads files from a stream of zip file bytes.
|
|
3
|
-
*/
|
|
4
|
-
import { IterableReadableStream } from '../utils/iterable-stream-polyfill';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
SIGNATURE_FILE,
|
|
8
|
-
SIGNATURE_CENTRAL_DIRECTORY,
|
|
9
|
-
SIGNATURE_CENTRAL_DIRECTORY_END,
|
|
10
|
-
FILE_HEADER_SIZE,
|
|
11
|
-
COMPRESSION_DEFLATE,
|
|
12
|
-
CompressionMethod,
|
|
13
|
-
} from './types';
|
|
14
|
-
import {
|
|
15
|
-
CentralDirectoryEntry,
|
|
16
|
-
FileEntry,
|
|
17
|
-
ZipEntry,
|
|
18
|
-
CentralDirectoryEndEntry,
|
|
19
|
-
} from './types';
|
|
20
|
-
import { filterStream } from '../utils/filter-stream';
|
|
21
|
-
import { collectBytes } from '../utils/collect-bytes';
|
|
22
|
-
import { limitBytes } from '../utils/limit-bytes';
|
|
23
|
-
import { concatBytes } from '../utils/concat-bytes';
|
|
24
|
-
import { prependBytes } from '../utils/prepend-bytes';
|
|
25
|
-
import { appendBytes } from '../utils/append-bytes';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Unzips a stream of zip file bytes.
|
|
29
|
-
*
|
|
30
|
-
* @param stream A stream of zip file bytes.
|
|
31
|
-
* @param predicate Optional. A function that returns true if the file should be downloaded.
|
|
32
|
-
* @returns An iterable stream of File objects.
|
|
33
|
-
*/
|
|
34
|
-
export function decodeZip(
|
|
35
|
-
stream: ReadableStream<Uint8Array>,
|
|
36
|
-
predicate?: () => boolean
|
|
37
|
-
) {
|
|
38
|
-
return streamZippedFileEntries(stream, predicate).pipeThrough(
|
|
39
|
-
new TransformStream<FileEntry, File>({
|
|
40
|
-
async transform(zipEntry, controller) {
|
|
41
|
-
const file = new File(
|
|
42
|
-
[zipEntry.bytes],
|
|
43
|
-
new TextDecoder().decode(zipEntry.path),
|
|
44
|
-
{
|
|
45
|
-
type: zipEntry.isDirectory ? 'directory' : undefined,
|
|
46
|
-
}
|
|
47
|
-
);
|
|
48
|
-
controller.enqueue(file);
|
|
49
|
-
},
|
|
50
|
-
})
|
|
51
|
-
) as IterableReadableStream<File>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const DEFAULT_PREDICATE = () => true;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Parses a stream of zipped bytes into FileEntry informations.
|
|
58
|
-
*
|
|
59
|
-
* @param stream A stream of zip file bytes.
|
|
60
|
-
* @param predicate Optional. A function that returns true if the file should be downloaded.
|
|
61
|
-
* @returns An iterable stream of FileEntry objects.
|
|
62
|
-
*/
|
|
63
|
-
export function streamZippedFileEntries(
|
|
64
|
-
stream: ReadableStream<Uint8Array>,
|
|
65
|
-
predicate: (
|
|
66
|
-
dirEntry: CentralDirectoryEntry | FileEntry
|
|
67
|
-
) => boolean = DEFAULT_PREDICATE
|
|
68
|
-
) {
|
|
69
|
-
const entriesStream = new ReadableStream<ZipEntry>({
|
|
70
|
-
async pull(controller) {
|
|
71
|
-
const entry = await nextZipEntry(stream);
|
|
72
|
-
if (!entry) {
|
|
73
|
-
controller.close();
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
controller.enqueue(entry);
|
|
77
|
-
},
|
|
78
|
-
}) as IterableReadableStream<ZipEntry>;
|
|
79
|
-
|
|
80
|
-
return entriesStream
|
|
81
|
-
.pipeThrough(
|
|
82
|
-
filterStream(({ signature }) => signature === SIGNATURE_FILE)
|
|
83
|
-
)
|
|
84
|
-
.pipeThrough(
|
|
85
|
-
filterStream(predicate as any)
|
|
86
|
-
) as IterableReadableStream<FileEntry>;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Reads the next zip entry from a stream of zip file bytes.
|
|
91
|
-
*
|
|
92
|
-
* @param stream A stream of zip file bytes.
|
|
93
|
-
* @returns A FileEntry object.
|
|
94
|
-
*/
|
|
95
|
-
async function nextZipEntry(stream: ReadableStream<Uint8Array>) {
|
|
96
|
-
const sigData = new DataView((await collectBytes(stream, 4))!.buffer);
|
|
97
|
-
const signature = sigData.getUint32(0, true);
|
|
98
|
-
if (signature === SIGNATURE_FILE) {
|
|
99
|
-
return await readFileEntry(stream, true);
|
|
100
|
-
} else if (signature === SIGNATURE_CENTRAL_DIRECTORY) {
|
|
101
|
-
return await readCentralDirectoryEntry(stream, true);
|
|
102
|
-
} else if (signature === SIGNATURE_CENTRAL_DIRECTORY_END) {
|
|
103
|
-
return await readEndCentralDirectoryEntry(stream, true);
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Reads a file entry from a zip file.
|
|
110
|
-
*
|
|
111
|
-
* The file entry is structured as follows:
|
|
112
|
-
*
|
|
113
|
-
* ```
|
|
114
|
-
* Offset Bytes Description
|
|
115
|
-
* 0 4 Local file header signature = 0x04034b50 (PK♥♦ or "PK\3\4")
|
|
116
|
-
* 4 2 Version needed to extract (minimum)
|
|
117
|
-
* 6 2 General purpose bit flag
|
|
118
|
-
* 8 2 Compression method; e.g. none = 0, DEFLATE = 8 (or "\0x08\0x00")
|
|
119
|
-
* 10 2 File last modification time
|
|
120
|
-
* 12 2 File last modification date
|
|
121
|
-
* 14 4 CRC-32 of uncompressed data
|
|
122
|
-
* 18 4 Compressed size (or 0xffffffff for ZIP64)
|
|
123
|
-
* 22 4 Uncompressed size (or 0xffffffff for ZIP64)
|
|
124
|
-
* 26 2 File name length (n)
|
|
125
|
-
* 28 2 Extra field length (m)
|
|
126
|
-
* 30 n File name
|
|
127
|
-
* 30+n m Extra field
|
|
128
|
-
* ```
|
|
129
|
-
*
|
|
130
|
-
* @param stream
|
|
131
|
-
* @param skipSignature Do not consume the signature from the stream.
|
|
132
|
-
* @returns
|
|
133
|
-
*/
|
|
134
|
-
export async function readFileEntry(
|
|
135
|
-
stream: ReadableStream<Uint8Array>,
|
|
136
|
-
skipSignature = false
|
|
137
|
-
): Promise<FileEntry | null> {
|
|
138
|
-
if (!skipSignature) {
|
|
139
|
-
const sigData = new DataView((await collectBytes(stream, 4))!.buffer);
|
|
140
|
-
const signature = sigData.getUint32(0, true);
|
|
141
|
-
if (signature !== SIGNATURE_FILE) {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const data = new DataView((await collectBytes(stream, 26))!.buffer);
|
|
146
|
-
const pathLength = data.getUint16(22, true);
|
|
147
|
-
const extraLength = data.getUint16(24, true);
|
|
148
|
-
const entry: Partial<FileEntry> = {
|
|
149
|
-
signature: SIGNATURE_FILE,
|
|
150
|
-
version: data.getUint32(0, true),
|
|
151
|
-
generalPurpose: data.getUint16(2, true),
|
|
152
|
-
compressionMethod: data.getUint16(4, true) as CompressionMethod,
|
|
153
|
-
lastModifiedTime: data.getUint16(6, true),
|
|
154
|
-
lastModifiedDate: data.getUint16(8, true),
|
|
155
|
-
crc: data.getUint32(10, true),
|
|
156
|
-
compressedSize: data.getUint32(14, true),
|
|
157
|
-
uncompressedSize: data.getUint32(18, true),
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
entry['path'] = await collectBytes(stream, pathLength);
|
|
161
|
-
entry['isDirectory'] = endsWithSlash(entry.path!);
|
|
162
|
-
entry['extra'] = await collectBytes(stream, extraLength);
|
|
163
|
-
|
|
164
|
-
// Make sure we consume the body stream or else
|
|
165
|
-
// we'll start reading the next file at the wrong
|
|
166
|
-
// offset.
|
|
167
|
-
// @TODO: Expose the body stream instead of reading it all
|
|
168
|
-
// eagerly. Ensure the next iteration exhausts
|
|
169
|
-
// the last body stream before moving on.
|
|
170
|
-
|
|
171
|
-
let bodyStream = limitBytes(stream, entry['compressedSize']!);
|
|
172
|
-
|
|
173
|
-
if (entry['compressionMethod'] === COMPRESSION_DEFLATE) {
|
|
174
|
-
/**
|
|
175
|
-
* We want to write raw deflate-compressed bytes into our
|
|
176
|
-
* final ZIP file. CompressionStream supports "deflate-raw"
|
|
177
|
-
* compression, but not on Node.js v18.
|
|
178
|
-
*
|
|
179
|
-
* As a workaround, we use the "gzip" compression and add
|
|
180
|
-
* the header and footer bytes. It works, because "gzip"
|
|
181
|
-
* compression is the same as "deflate" compression plus
|
|
182
|
-
* the header and the footer.
|
|
183
|
-
*
|
|
184
|
-
* The header is 10 bytes long:
|
|
185
|
-
* - 2 magic bytes: 0x1f, 0x8b
|
|
186
|
-
* - 1 compression method: 0x08 (deflate)
|
|
187
|
-
* - 1 header flags
|
|
188
|
-
* - 4 mtime: 0x00000000 (no timestamp)
|
|
189
|
-
* - 1 compression flags
|
|
190
|
-
* - 1 OS: 0x03 (Unix)
|
|
191
|
-
*
|
|
192
|
-
* The footer is 8 bytes long:
|
|
193
|
-
* - 4 bytes for CRC32 of the uncompressed data
|
|
194
|
-
* - 4 bytes for ISIZE (uncompressed size modulo 2^32)
|
|
195
|
-
*/
|
|
196
|
-
const header = new Uint8Array(10);
|
|
197
|
-
header.set([0x1f, 0x8b, 0x08]);
|
|
198
|
-
|
|
199
|
-
const footer = new Uint8Array(8);
|
|
200
|
-
const footerView = new DataView(footer.buffer);
|
|
201
|
-
footerView.setUint32(0, entry.crc!, true);
|
|
202
|
-
footerView.setUint32(4, entry.uncompressedSize! % 2 ** 32, true);
|
|
203
|
-
bodyStream = bodyStream
|
|
204
|
-
.pipeThrough(prependBytes(header))
|
|
205
|
-
.pipeThrough(appendBytes(footer))
|
|
206
|
-
.pipeThrough(new DecompressionStream('gzip'));
|
|
207
|
-
}
|
|
208
|
-
entry['bytes'] = await bodyStream
|
|
209
|
-
.pipeThrough(concatBytes(entry['uncompressedSize']))
|
|
210
|
-
.getReader()
|
|
211
|
-
.read()
|
|
212
|
-
.then(({ value }) => value!);
|
|
213
|
-
return entry as FileEntry;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Reads a central directory entry from a zip file.
|
|
218
|
-
*
|
|
219
|
-
* The central directory entry is structured as follows:
|
|
220
|
-
*
|
|
221
|
-
* ```
|
|
222
|
-
* Offset Bytes Description
|
|
223
|
-
* 0 4 Central directory file header signature = 0x02014b50
|
|
224
|
-
* 4 2 Version made by
|
|
225
|
-
* 6 2 Version needed to extract (minimum)
|
|
226
|
-
* 8 2 General purpose bit flag
|
|
227
|
-
* 10 2 Compression method
|
|
228
|
-
* 12 2 File last modification time
|
|
229
|
-
* 14 2 File last modification date
|
|
230
|
-
* 16 4 CRC-32 of uncompressed data
|
|
231
|
-
* 20 4 Compressed size (or 0xffffffff for ZIP64)
|
|
232
|
-
* 24 4 Uncompressed size (or 0xffffffff for ZIP64)
|
|
233
|
-
* 28 2 File name length (n)
|
|
234
|
-
* 30 2 Extra field length (m)
|
|
235
|
-
* 32 2 File comment length (k)
|
|
236
|
-
* 34 2 Disk number where file starts (or 0xffff for ZIP64)
|
|
237
|
-
* 36 2 Internal file attributes
|
|
238
|
-
* 38 4 External file attributes
|
|
239
|
-
* 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.
|
|
240
|
-
* 46 n File name
|
|
241
|
-
* 46+n m Extra field
|
|
242
|
-
* 46+n+m k File comment
|
|
243
|
-
* ```
|
|
244
|
-
*
|
|
245
|
-
* @param stream
|
|
246
|
-
* @param skipSignature
|
|
247
|
-
* @returns
|
|
248
|
-
*/
|
|
249
|
-
export async function readCentralDirectoryEntry(
|
|
250
|
-
stream: ReadableStream<Uint8Array>,
|
|
251
|
-
skipSignature = false
|
|
252
|
-
): Promise<CentralDirectoryEntry | null> {
|
|
253
|
-
if (!skipSignature) {
|
|
254
|
-
const sigData = new DataView((await collectBytes(stream, 4))!.buffer);
|
|
255
|
-
const signature = sigData.getUint32(0, true);
|
|
256
|
-
if (signature !== SIGNATURE_CENTRAL_DIRECTORY) {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
const data = new DataView((await collectBytes(stream, 42))!.buffer);
|
|
261
|
-
const pathLength = data.getUint16(24, true);
|
|
262
|
-
const extraLength = data.getUint16(26, true);
|
|
263
|
-
const fileCommentLength = data.getUint16(28, true);
|
|
264
|
-
const centralDirectory: Partial<CentralDirectoryEntry> = {
|
|
265
|
-
signature: SIGNATURE_CENTRAL_DIRECTORY,
|
|
266
|
-
versionCreated: data.getUint16(0, true),
|
|
267
|
-
versionNeeded: data.getUint16(2, true),
|
|
268
|
-
generalPurpose: data.getUint16(4, true),
|
|
269
|
-
compressionMethod: data.getUint16(6, true) as CompressionMethod,
|
|
270
|
-
lastModifiedTime: data.getUint16(8, true),
|
|
271
|
-
lastModifiedDate: data.getUint16(10, true),
|
|
272
|
-
crc: data.getUint32(12, true),
|
|
273
|
-
compressedSize: data.getUint32(16, true),
|
|
274
|
-
uncompressedSize: data.getUint32(20, true),
|
|
275
|
-
diskNumber: data.getUint16(30, true),
|
|
276
|
-
internalAttributes: data.getUint16(32, true),
|
|
277
|
-
externalAttributes: data.getUint32(34, true),
|
|
278
|
-
firstByteAt: data.getUint32(38, true),
|
|
279
|
-
};
|
|
280
|
-
centralDirectory['lastByteAt'] =
|
|
281
|
-
centralDirectory.firstByteAt! +
|
|
282
|
-
FILE_HEADER_SIZE +
|
|
283
|
-
pathLength +
|
|
284
|
-
fileCommentLength +
|
|
285
|
-
extraLength! +
|
|
286
|
-
centralDirectory.compressedSize! -
|
|
287
|
-
1;
|
|
288
|
-
|
|
289
|
-
centralDirectory['path'] = await collectBytes(stream, pathLength);
|
|
290
|
-
centralDirectory['isDirectory'] = endsWithSlash(centralDirectory.path!);
|
|
291
|
-
centralDirectory['extra'] = await collectBytes(stream, extraLength);
|
|
292
|
-
centralDirectory['fileComment'] = await collectBytes(
|
|
293
|
-
stream,
|
|
294
|
-
fileCommentLength
|
|
295
|
-
);
|
|
296
|
-
return centralDirectory as CentralDirectoryEntry;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function endsWithSlash(path: Uint8Array) {
|
|
300
|
-
return path[path.byteLength - 1] == '/'.charCodeAt(0);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Reads the end of central directory entry from a zip file.
|
|
305
|
-
*
|
|
306
|
-
* The end of central directory entry is structured as follows:
|
|
307
|
-
*
|
|
308
|
-
* ```
|
|
309
|
-
* Offset Bytes Description[33]
|
|
310
|
-
* 0 4 End of central directory signature = 0x06054b50
|
|
311
|
-
* 4 2 Number of this disk (or 0xffff for ZIP64)
|
|
312
|
-
* 6 2 Disk where central directory starts (or 0xffff for ZIP64)
|
|
313
|
-
* 8 2 Number of central directory records on this disk (or 0xffff for ZIP64)
|
|
314
|
-
* 10 2 Total number of central directory records (or 0xffff for ZIP64)
|
|
315
|
-
* 12 4 Size of central directory (bytes) (or 0xffffffff for ZIP64)
|
|
316
|
-
* 16 4 Offset of start of central directory, relative to start of archive (or 0xffffffff for ZIP64)
|
|
317
|
-
* 20 2 Comment length (n)
|
|
318
|
-
* 22 n Comment
|
|
319
|
-
* ```
|
|
320
|
-
*
|
|
321
|
-
* @param stream
|
|
322
|
-
* @param skipSignature
|
|
323
|
-
* @returns
|
|
324
|
-
*/
|
|
325
|
-
async function readEndCentralDirectoryEntry(
|
|
326
|
-
stream: ReadableStream<Uint8Array>,
|
|
327
|
-
skipSignature = false
|
|
328
|
-
) {
|
|
329
|
-
if (!skipSignature) {
|
|
330
|
-
const sigData = new DataView((await collectBytes(stream, 4))!.buffer);
|
|
331
|
-
const signature = sigData.getUint32(0, true);
|
|
332
|
-
if (signature !== SIGNATURE_CENTRAL_DIRECTORY_END) {
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
const data = new DataView((await collectBytes(stream, 18))!.buffer);
|
|
337
|
-
const endOfDirectory: Partial<CentralDirectoryEndEntry> = {
|
|
338
|
-
signature: SIGNATURE_CENTRAL_DIRECTORY_END,
|
|
339
|
-
numberOfDisks: data.getUint16(0, true),
|
|
340
|
-
centralDirectoryStartDisk: data.getUint16(2, true),
|
|
341
|
-
numberCentralDirectoryRecordsOnThisDisk: data.getUint16(4, true),
|
|
342
|
-
numberCentralDirectoryRecords: data.getUint16(6, true),
|
|
343
|
-
centralDirectorySize: data.getUint32(8, true),
|
|
344
|
-
centralDirectoryOffset: data.getUint32(12, true),
|
|
345
|
-
};
|
|
346
|
-
const commentLength = data.getUint16(16, true);
|
|
347
|
-
endOfDirectory['comment'] = await collectBytes(stream, commentLength);
|
|
348
|
-
return endOfDirectory as CentralDirectoryEndEntry;
|
|
349
|
-
}
|
package/src/zip/encode-zip.ts
DELETED
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
COMPRESSION_DEFLATE,
|
|
3
|
-
COMPRESSION_NONE,
|
|
4
|
-
CentralDirectoryEndEntry,
|
|
5
|
-
CentralDirectoryEntry,
|
|
6
|
-
FileHeader,
|
|
7
|
-
} from './types';
|
|
8
|
-
import {
|
|
9
|
-
SIGNATURE_CENTRAL_DIRECTORY_END,
|
|
10
|
-
SIGNATURE_CENTRAL_DIRECTORY,
|
|
11
|
-
SIGNATURE_FILE,
|
|
12
|
-
} from './types';
|
|
13
|
-
import { iteratorToStream } from '../utils/iterator-to-stream';
|
|
14
|
-
import { collectBytes } from '../utils/collect-bytes';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Compresses the given files into a ZIP archive.
|
|
18
|
-
*
|
|
19
|
-
* @param files - An async or sync iterable of files to be compressed.
|
|
20
|
-
* @returns A readable stream of the compressed ZIP archive as Uint8Array chunks.
|
|
21
|
-
*/
|
|
22
|
-
export function encodeZip(
|
|
23
|
-
files: AsyncIterable<File> | Iterable<File>
|
|
24
|
-
): ReadableStream<Uint8Array> {
|
|
25
|
-
return iteratorToStream(files).pipeThrough(encodeZipTransform());
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Encodes the files into a ZIP format.
|
|
30
|
-
*
|
|
31
|
-
* @returns A stream transforming File objects into zipped bytes.
|
|
32
|
-
*/
|
|
33
|
-
function encodeZipTransform() {
|
|
34
|
-
const offsetToFileHeaderMap: Map<number, FileHeader> = new Map();
|
|
35
|
-
let writtenBytes = 0;
|
|
36
|
-
return new TransformStream<File, Uint8Array>({
|
|
37
|
-
async transform(file, controller) {
|
|
38
|
-
const entryBytes = new Uint8Array(await file.arrayBuffer());
|
|
39
|
-
/**
|
|
40
|
-
* We want to write raw deflate-compressed bytes into our
|
|
41
|
-
* final ZIP file. CompressionStream supports "deflate-raw"
|
|
42
|
-
* compression, but not on Node.js v18.
|
|
43
|
-
*
|
|
44
|
-
* As a workaround, we use the "gzip" compression and add
|
|
45
|
-
* the header and footer bytes. It works, because "gzip"
|
|
46
|
-
* compression is the same as "deflate" compression plus
|
|
47
|
-
* the header and the footer.
|
|
48
|
-
*
|
|
49
|
-
* The header is 10 bytes long:
|
|
50
|
-
* - 2 magic bytes: 0x1f, 0x8b
|
|
51
|
-
* - 1 compression method: 0x08 (deflate)
|
|
52
|
-
* - 1 header flags
|
|
53
|
-
* - 4 mtime: 0x00000000 (no timestamp)
|
|
54
|
-
* - 1 compression flags
|
|
55
|
-
* - 1 OS: 0x03 (Unix)
|
|
56
|
-
*
|
|
57
|
-
* The footer is 8 bytes long:
|
|
58
|
-
* - 4 bytes for CRC32 of the uncompressed data
|
|
59
|
-
* - 4 bytes for ISIZE (uncompressed size modulo 2^32)
|
|
60
|
-
*/
|
|
61
|
-
let compressed = (await collectBytes(
|
|
62
|
-
new Blob([entryBytes])
|
|
63
|
-
.stream()
|
|
64
|
-
.pipeThrough(new CompressionStream('gzip'))
|
|
65
|
-
))!;
|
|
66
|
-
// Grab the CRC32 hash from the footer.
|
|
67
|
-
const crcHash = new DataView(compressed.buffer).getUint32(
|
|
68
|
-
compressed.byteLength - 8,
|
|
69
|
-
true
|
|
70
|
-
);
|
|
71
|
-
// Strip the header and the footer.
|
|
72
|
-
compressed = compressed.slice(10, compressed.byteLength - 8);
|
|
73
|
-
|
|
74
|
-
const encodedPath = new TextEncoder().encode(file.name);
|
|
75
|
-
const zipFileEntry: FileHeader = {
|
|
76
|
-
signature: SIGNATURE_FILE,
|
|
77
|
-
version: 2,
|
|
78
|
-
generalPurpose: 0,
|
|
79
|
-
compressionMethod:
|
|
80
|
-
file.type === 'directory' || compressed.byteLength === 0
|
|
81
|
-
? COMPRESSION_NONE
|
|
82
|
-
: COMPRESSION_DEFLATE,
|
|
83
|
-
lastModifiedTime: 0,
|
|
84
|
-
lastModifiedDate: 0,
|
|
85
|
-
crc: crcHash,
|
|
86
|
-
compressedSize: compressed.byteLength,
|
|
87
|
-
uncompressedSize: entryBytes.byteLength,
|
|
88
|
-
path: encodedPath,
|
|
89
|
-
extra: new Uint8Array(0),
|
|
90
|
-
};
|
|
91
|
-
offsetToFileHeaderMap.set(writtenBytes, zipFileEntry);
|
|
92
|
-
|
|
93
|
-
const headerBytes = encodeFileEntryHeader(zipFileEntry);
|
|
94
|
-
controller.enqueue(headerBytes);
|
|
95
|
-
writtenBytes += headerBytes.byteLength;
|
|
96
|
-
|
|
97
|
-
controller.enqueue(compressed);
|
|
98
|
-
writtenBytes += compressed.byteLength;
|
|
99
|
-
},
|
|
100
|
-
flush(controller) {
|
|
101
|
-
const centralDirectoryOffset = writtenBytes;
|
|
102
|
-
let centralDirectorySize = 0;
|
|
103
|
-
for (const [
|
|
104
|
-
fileOffset,
|
|
105
|
-
header,
|
|
106
|
-
] of offsetToFileHeaderMap.entries()) {
|
|
107
|
-
const centralDirectoryEntry: Partial<CentralDirectoryEntry> = {
|
|
108
|
-
...header,
|
|
109
|
-
signature: SIGNATURE_CENTRAL_DIRECTORY,
|
|
110
|
-
fileComment: new Uint8Array(0),
|
|
111
|
-
diskNumber: 1,
|
|
112
|
-
internalAttributes: 0,
|
|
113
|
-
externalAttributes: 0,
|
|
114
|
-
firstByteAt: fileOffset,
|
|
115
|
-
};
|
|
116
|
-
const centralDirectoryEntryBytes = encodeCentralDirectoryEntry(
|
|
117
|
-
centralDirectoryEntry as CentralDirectoryEntry,
|
|
118
|
-
fileOffset
|
|
119
|
-
);
|
|
120
|
-
controller.enqueue(centralDirectoryEntryBytes);
|
|
121
|
-
centralDirectorySize += centralDirectoryEntryBytes.byteLength;
|
|
122
|
-
}
|
|
123
|
-
const centralDirectoryEnd: CentralDirectoryEndEntry = {
|
|
124
|
-
signature: SIGNATURE_CENTRAL_DIRECTORY_END,
|
|
125
|
-
numberOfDisks: 1,
|
|
126
|
-
centralDirectoryOffset,
|
|
127
|
-
centralDirectorySize,
|
|
128
|
-
centralDirectoryStartDisk: 1,
|
|
129
|
-
numberCentralDirectoryRecordsOnThisDisk:
|
|
130
|
-
offsetToFileHeaderMap.size,
|
|
131
|
-
numberCentralDirectoryRecords: offsetToFileHeaderMap.size,
|
|
132
|
-
comment: new Uint8Array(0),
|
|
133
|
-
};
|
|
134
|
-
const centralDirectoryEndBytes =
|
|
135
|
-
encodeCentralDirectoryEnd(centralDirectoryEnd);
|
|
136
|
-
controller.enqueue(centralDirectoryEndBytes);
|
|
137
|
-
offsetToFileHeaderMap.clear();
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Encodes a file entry header as a Uint8Array.
|
|
144
|
-
*
|
|
145
|
-
* The array is structured as follows:
|
|
146
|
-
*
|
|
147
|
-
* ```
|
|
148
|
-
* Offset Bytes Description
|
|
149
|
-
* 0 4 Local file header signature = 0x04034b50 (PK♥♦ or "PK\3\4")
|
|
150
|
-
* 4 2 Version needed to extract (minimum)
|
|
151
|
-
* 6 2 General purpose bit flag
|
|
152
|
-
* 8 2 Compression method; e.g. none = 0, DEFLATE = 8 (or "\0x08\0x00")
|
|
153
|
-
* 10 2 File last modification time
|
|
154
|
-
* 12 2 File last modification date
|
|
155
|
-
* 14 4 CRC-32 of uncompressed data
|
|
156
|
-
* 18 4 Compressed size (or 0xffffffff for ZIP64)
|
|
157
|
-
* 22 4 Uncompressed size (or 0xffffffff for ZIP64)
|
|
158
|
-
* 26 2 File name length (n)
|
|
159
|
-
* 28 2 Extra field length (m)
|
|
160
|
-
* 30 n File name
|
|
161
|
-
* 30+n m Extra field
|
|
162
|
-
* ```
|
|
163
|
-
*/
|
|
164
|
-
function encodeFileEntryHeader(entry: FileHeader) {
|
|
165
|
-
const buffer = new ArrayBuffer(
|
|
166
|
-
30 + entry.path.byteLength + entry.extra.byteLength
|
|
167
|
-
);
|
|
168
|
-
const view = new DataView(buffer);
|
|
169
|
-
view.setUint32(0, entry.signature, true);
|
|
170
|
-
view.setUint16(4, entry.version, true);
|
|
171
|
-
view.setUint16(6, entry.generalPurpose, true);
|
|
172
|
-
view.setUint16(8, entry.compressionMethod, true);
|
|
173
|
-
view.setUint16(10, entry.lastModifiedDate, true);
|
|
174
|
-
view.setUint16(12, entry.lastModifiedTime, true);
|
|
175
|
-
view.setUint32(14, entry.crc, true);
|
|
176
|
-
view.setUint32(18, entry.compressedSize, true);
|
|
177
|
-
view.setUint32(22, entry.uncompressedSize, true);
|
|
178
|
-
view.setUint16(26, entry.path.byteLength, true);
|
|
179
|
-
view.setUint16(28, entry.extra.byteLength, true);
|
|
180
|
-
const uint8Header = new Uint8Array(buffer);
|
|
181
|
-
uint8Header.set(entry.path, 30);
|
|
182
|
-
uint8Header.set(entry.extra, 30 + entry.path.byteLength);
|
|
183
|
-
return uint8Header;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Encodes a central directory entry as a Uint8Array.
|
|
188
|
-
*
|
|
189
|
-
* The central directory entry is structured as follows:
|
|
190
|
-
*
|
|
191
|
-
* ```
|
|
192
|
-
* Offset Bytes Description
|
|
193
|
-
* 0 4 Central directory file header signature = 0x02014b50
|
|
194
|
-
* 4 2 Version made by
|
|
195
|
-
* 6 2 Version needed to extract (minimum)
|
|
196
|
-
* 8 2 General purpose bit flag
|
|
197
|
-
* 10 2 Compression method
|
|
198
|
-
* 12 2 File last modification time
|
|
199
|
-
* 14 2 File last modification date
|
|
200
|
-
* 16 4 CRC-32 of uncompressed data
|
|
201
|
-
* 20 4 Compressed size (or 0xffffffff for ZIP64)
|
|
202
|
-
* 24 4 Uncompressed size (or 0xffffffff for ZIP64)
|
|
203
|
-
* 28 2 File name length (n)
|
|
204
|
-
* 30 2 Extra field length (m)
|
|
205
|
-
* 32 2 File comment length (k)
|
|
206
|
-
* 34 2 Disk number where file starts (or 0xffff for ZIP64)
|
|
207
|
-
* 36 2 Internal file attributes
|
|
208
|
-
* 38 4 External file attributes
|
|
209
|
-
* 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.
|
|
210
|
-
* 46 n File name
|
|
211
|
-
* 46+n m Extra field
|
|
212
|
-
* 46+n+m k File comment
|
|
213
|
-
* ```
|
|
214
|
-
*/
|
|
215
|
-
function encodeCentralDirectoryEntry(
|
|
216
|
-
entry: CentralDirectoryEntry,
|
|
217
|
-
fileEntryOffset: number
|
|
218
|
-
) {
|
|
219
|
-
const buffer = new ArrayBuffer(
|
|
220
|
-
46 + entry.path.byteLength + entry.extra.byteLength
|
|
221
|
-
);
|
|
222
|
-
const view = new DataView(buffer);
|
|
223
|
-
view.setUint32(0, entry.signature, true);
|
|
224
|
-
view.setUint16(4, entry.versionCreated, true);
|
|
225
|
-
view.setUint16(6, entry.versionNeeded, true);
|
|
226
|
-
view.setUint16(8, entry.generalPurpose, true);
|
|
227
|
-
view.setUint16(10, entry.compressionMethod, true);
|
|
228
|
-
view.setUint16(12, entry.lastModifiedDate, true);
|
|
229
|
-
view.setUint16(14, entry.lastModifiedTime, true);
|
|
230
|
-
view.setUint32(16, entry.crc, true);
|
|
231
|
-
view.setUint32(20, entry.compressedSize, true);
|
|
232
|
-
view.setUint32(24, entry.uncompressedSize, true);
|
|
233
|
-
view.setUint16(28, entry.path.byteLength, true);
|
|
234
|
-
view.setUint16(30, entry.extra.byteLength, true);
|
|
235
|
-
view.setUint16(32, entry.fileComment.byteLength, true);
|
|
236
|
-
view.setUint16(34, entry.diskNumber, true);
|
|
237
|
-
view.setUint16(36, entry.internalAttributes, true);
|
|
238
|
-
view.setUint32(38, entry.externalAttributes, true);
|
|
239
|
-
view.setUint32(42, fileEntryOffset, true);
|
|
240
|
-
const uint8Header = new Uint8Array(buffer);
|
|
241
|
-
uint8Header.set(entry.path, 46);
|
|
242
|
-
uint8Header.set(entry.extra, 46 + entry.path.byteLength);
|
|
243
|
-
return uint8Header;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Encodes the end of central directory entry as a Uint8Array.
|
|
248
|
-
*
|
|
249
|
-
* The end of central directory entry is structured as follows:
|
|
250
|
-
*
|
|
251
|
-
* ```
|
|
252
|
-
* Offset Bytes Description[33]
|
|
253
|
-
* 0 4 End of central directory signature = 0x06054b50
|
|
254
|
-
* 4 2 Number of this disk (or 0xffff for ZIP64)
|
|
255
|
-
* 6 2 Disk where central directory starts (or 0xffff for ZIP64)
|
|
256
|
-
* 8 2 Number of central directory records on this disk (or 0xffff for ZIP64)
|
|
257
|
-
* 10 2 Total number of central directory records (or 0xffff for ZIP64)
|
|
258
|
-
* 12 4 Size of central directory (bytes) (or 0xffffffff for ZIP64)
|
|
259
|
-
* 16 4 Offset of start of central directory, relative to start of archive (or 0xffffffff for ZIP64)
|
|
260
|
-
* 20 2 Comment length (n)
|
|
261
|
-
* 22 n Comment
|
|
262
|
-
* ```
|
|
263
|
-
*/
|
|
264
|
-
function encodeCentralDirectoryEnd(entry: CentralDirectoryEndEntry) {
|
|
265
|
-
const buffer = new ArrayBuffer(22 + entry.comment.byteLength);
|
|
266
|
-
const view = new DataView(buffer);
|
|
267
|
-
view.setUint32(0, entry.signature, true);
|
|
268
|
-
view.setUint16(4, entry.numberOfDisks, true);
|
|
269
|
-
view.setUint16(6, entry.centralDirectoryStartDisk, true);
|
|
270
|
-
view.setUint16(8, entry.numberCentralDirectoryRecordsOnThisDisk, true);
|
|
271
|
-
view.setUint16(10, entry.numberCentralDirectoryRecords, true);
|
|
272
|
-
view.setUint32(12, entry.centralDirectorySize, true);
|
|
273
|
-
view.setUint32(16, entry.centralDirectoryOffset, true);
|
|
274
|
-
view.setUint16(20, entry.comment.byteLength, true);
|
|
275
|
-
const uint8Header = new Uint8Array(buffer);
|
|
276
|
-
uint8Header.set(entry.comment, 22);
|
|
277
|
-
return uint8Header;
|
|
278
|
-
}
|
package/src/zip/types.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
export const FILE_HEADER_SIZE = 32;
|
|
2
|
-
export const SIGNATURE_FILE = 67324752 as const;
|
|
3
|
-
export const SIGNATURE_CENTRAL_DIRECTORY = 33639248 as const;
|
|
4
|
-
export const SIGNATURE_CENTRAL_DIRECTORY_END = 101010256 as const;
|
|
5
|
-
export const SIGNATURE_DATA_DESCRIPTOR = 134695760 as const;
|
|
6
|
-
|
|
7
|
-
export const COMPRESSION_NONE = 0 as const;
|
|
8
|
-
export const COMPRESSION_DEFLATE = 8 as const;
|
|
9
|
-
export type CompressionMethod =
|
|
10
|
-
| typeof COMPRESSION_NONE
|
|
11
|
-
| typeof COMPRESSION_DEFLATE;
|
|
12
|
-
|
|
13
|
-
export type ZipEntry =
|
|
14
|
-
| FileEntry
|
|
15
|
-
| CentralDirectoryEntry
|
|
16
|
-
| CentralDirectoryEndEntry;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Data of the file entry header encoded in a ".zip" file.
|
|
20
|
-
*/
|
|
21
|
-
export interface FileHeader {
|
|
22
|
-
signature: typeof SIGNATURE_FILE;
|
|
23
|
-
version: number;
|
|
24
|
-
generalPurpose: number;
|
|
25
|
-
compressionMethod: CompressionMethod;
|
|
26
|
-
lastModifiedTime: number;
|
|
27
|
-
lastModifiedDate: number;
|
|
28
|
-
crc: number;
|
|
29
|
-
compressedSize: number;
|
|
30
|
-
uncompressedSize: number;
|
|
31
|
-
path: Uint8Array;
|
|
32
|
-
extra: Uint8Array;
|
|
33
|
-
}
|
|
34
|
-
export interface FileEntry extends FileHeader {
|
|
35
|
-
isDirectory: boolean;
|
|
36
|
-
bytes: Uint8Array;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Data of the central directory entry encoded in a ".zip" file.
|
|
41
|
-
*/
|
|
42
|
-
export interface CentralDirectoryEntry {
|
|
43
|
-
signature: typeof SIGNATURE_CENTRAL_DIRECTORY;
|
|
44
|
-
versionCreated: number;
|
|
45
|
-
versionNeeded: number;
|
|
46
|
-
generalPurpose: number;
|
|
47
|
-
compressionMethod: CompressionMethod;
|
|
48
|
-
lastModifiedTime: number;
|
|
49
|
-
lastModifiedDate: number;
|
|
50
|
-
crc: number;
|
|
51
|
-
compressedSize: number;
|
|
52
|
-
uncompressedSize: number;
|
|
53
|
-
diskNumber: number;
|
|
54
|
-
internalAttributes: number;
|
|
55
|
-
externalAttributes: number;
|
|
56
|
-
firstByteAt: number;
|
|
57
|
-
lastByteAt: number;
|
|
58
|
-
path: Uint8Array;
|
|
59
|
-
extra: Uint8Array;
|
|
60
|
-
fileComment: Uint8Array;
|
|
61
|
-
isDirectory: boolean;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Data of the central directory end entry encoded in a ".zip" file.
|
|
66
|
-
*/
|
|
67
|
-
export interface CentralDirectoryEndEntry {
|
|
68
|
-
signature: typeof SIGNATURE_CENTRAL_DIRECTORY_END;
|
|
69
|
-
numberOfDisks: number;
|
|
70
|
-
centralDirectoryStartDisk: number;
|
|
71
|
-
numberCentralDirectoryRecordsOnThisDisk: number;
|
|
72
|
-
numberCentralDirectoryRecords: number;
|
|
73
|
-
centralDirectorySize: number;
|
|
74
|
-
centralDirectoryOffset: number;
|
|
75
|
-
comment: Uint8Array;
|
|
76
|
-
}
|