@quentinadam/zip 0.1.4

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/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @quentinadam/zip
2
+
3
+ [![JSR][jsr-image]][jsr-url] [![NPM][npm-image]][npm-url] [![CI][ci-image]][ci-url]
4
+
5
+ A library for creating and extracting ZIP archives.
6
+
7
+ ## Usage
8
+
9
+ ```ts
10
+ import * as zip from '@quentinadam/zip';
11
+
12
+ const files = [
13
+ { name: 'hello.txt', data: new TextEncoder().encode('hello world') },
14
+ { name: 'ipsum/lorem.txt', data: new TextEncoder().encode('ipsum lorem') },
15
+ ];
16
+
17
+ const buffer = await zip.create(files);
18
+
19
+ for (const { name, data } of await zip.extract(buffer)) {
20
+ console.log(name, new TextDecoder().decode(data));
21
+ }
22
+ ```
23
+
24
+ [ci-image]: https://img.shields.io/github/actions/workflow/status/quentinadam/deno-zip/ci.yml?branch=main&logo=github&style=flat-square
25
+ [ci-url]: https://github.com/quentinadam/deno-zip/actions/workflows/ci.yml
26
+ [npm-image]: https://img.shields.io/npm/v/@quentinadam/zip.svg?style=flat-square
27
+ [npm-url]: https://npmjs.org/package/@quentinadam/zip
28
+ [jsr-image]: https://jsr.io/badges/@quentinadam/zip?style=flat-square
29
+ [jsr-url]: https://jsr.io/@quentinadam/zip
@@ -0,0 +1 @@
1
+ export default function crc32(buffer: Uint8Array, crc?: number): number;
package/dist/crc32.js ADDED
@@ -0,0 +1,19 @@
1
+ import require from '@quentinadam/require';
2
+ const POLYNOMIAL = -306674912;
3
+ const TABLE = /* @__PURE__ */ (() => {
4
+ const table = new Int32Array(256);
5
+ for (let i = 0; i < 256; i++) {
6
+ let r = i;
7
+ for (let bit = 8; bit > 0; --bit) {
8
+ r = (r & 1) ? ((r >>> 1) ^ POLYNOMIAL) : (r >>> 1);
9
+ }
10
+ table[i] = r;
11
+ }
12
+ return table;
13
+ })();
14
+ export default function crc32(buffer, crc = 0xFFFFFFFF) {
15
+ for (const byte of buffer) {
16
+ crc = require(TABLE[(crc ^ byte) & 0xff]) ^ (crc >>> 8);
17
+ }
18
+ return (crc ^ -1) >>> 0;
19
+ }
@@ -0,0 +1,2 @@
1
+ export declare function decompress(buffer: Uint8Array, raw?: boolean): Promise<Uint8Array<ArrayBufferLike>>;
2
+ export declare function compress(buffer: Uint8Array, raw?: boolean): Promise<Uint8Array<ArrayBufferLike>>;
@@ -0,0 +1,19 @@
1
+ import * as Uint8ArrayExtension from '@quentinadam/uint8array-extension';
2
+ async function transform(stream, data) {
3
+ const writer = stream.writable.getWriter();
4
+ writer.write(data);
5
+ writer.close();
6
+ const chunks = [];
7
+ for await (const chunk of stream.readable) {
8
+ chunks.push(chunk);
9
+ }
10
+ return Uint8ArrayExtension.concat(chunks);
11
+ }
12
+ export async function decompress(buffer, raw = false) {
13
+ const stream = new DecompressionStream(raw ? 'deflate-raw' : 'deflate');
14
+ return await transform(stream, buffer);
15
+ }
16
+ export async function compress(buffer, raw = false) {
17
+ const stream = new CompressionStream(raw ? 'deflate-raw' : 'deflate');
18
+ return await transform(stream, buffer);
19
+ }
package/dist/zip.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Extracts files from a ZIP archive.
3
+ * @param buffer An Uint8Array containing the ZIP archive.
4
+ * @returns A list of files extracted from the ZIP archive.
5
+ */
6
+ export declare function extract(buffer: Uint8Array): Promise<{
7
+ name: string;
8
+ data: Uint8Array;
9
+ lastModification: Date;
10
+ }[]>;
11
+ /**
12
+ * Creates a ZIP archive from a list of files.
13
+ * @param files List of files to include in the archive.
14
+ * @returns An Uint8Array containing the ZIP archive.
15
+ */
16
+ export declare function create(files: {
17
+ name: string;
18
+ data: Uint8Array;
19
+ lastModification?: Date;
20
+ }[]): Promise<Uint8Array>;
package/dist/zip.js ADDED
@@ -0,0 +1,349 @@
1
+ import assert from '@quentinadam/assert';
2
+ import Uint8ArrayExtension from '@quentinadam/uint8array-extension';
3
+ import crc32 from "./crc32.js";
4
+ import { compress, decompress } from "./deflate.js";
5
+ const FILE_ENTRY_SIGNATURE = 0x04034b50;
6
+ const DIRECTORY_ENTRY_SIGNATURE = 0x02014b50;
7
+ const DIRECTORY_SIGNATURE = 0x06054b50;
8
+ class Reader {
9
+ #buffer;
10
+ #offset;
11
+ constructor(buffer, offset = 0) {
12
+ this.#buffer = buffer;
13
+ this.#offset = offset;
14
+ }
15
+ seek(offset) {
16
+ this.#offset = offset;
17
+ }
18
+ #read(fn, length) {
19
+ const result = fn();
20
+ this.#offset += length;
21
+ return result;
22
+ }
23
+ readUint16LE() {
24
+ return this.#read(() => new Uint8ArrayExtension(this.#buffer).getUint16LE(this.#offset), 2);
25
+ }
26
+ readUint32LE() {
27
+ return this.#read(() => new Uint8ArrayExtension(this.#buffer).getUint32LE(this.#offset), 4);
28
+ }
29
+ readBuffer(length) {
30
+ return this.#read(() => this.#buffer.slice(this.#offset, this.#offset + length), length);
31
+ }
32
+ locateDirectory() {
33
+ const needle = Uint8ArrayExtension.fromUint32LE(DIRECTORY_SIGNATURE);
34
+ for (let i = this.#buffer.length - needle.length; i >= 0; i--) {
35
+ const result = (() => {
36
+ for (let j = 0; j < needle.length; j++) {
37
+ if (this.#buffer[i + j] !== needle[j]) {
38
+ return false;
39
+ }
40
+ }
41
+ return true;
42
+ })();
43
+ if (result) {
44
+ this.#offset = i;
45
+ return;
46
+ }
47
+ }
48
+ throw new Error('Could not locate directory');
49
+ }
50
+ readDirectory() {
51
+ assert(this.readUint32LE() === DIRECTORY_SIGNATURE);
52
+ const countDisks = this.readUint16LE();
53
+ const diskNumber = this.readUint16LE();
54
+ const countDiskRecords = this.readUint16LE();
55
+ const countRecords = this.readUint16LE();
56
+ const size = this.readUint32LE();
57
+ const offset = this.readUint32LE();
58
+ const commentLength = this.readUint16LE();
59
+ const comment = this.readBuffer(commentLength);
60
+ return { countDisks, diskNumber, countDiskRecords, countRecords, size, offset, comment };
61
+ }
62
+ readDirectoryEntry() {
63
+ assert(this.readUint32LE() === DIRECTORY_ENTRY_SIGNATURE);
64
+ const version = this.readUint16LE();
65
+ const requiredVersion = this.readUint16LE();
66
+ const flag = this.readUint16LE();
67
+ const compressionMethod = this.readUint16LE();
68
+ const lastModificationTime = this.readUint16LE();
69
+ const lastModificationDate = this.readUint16LE();
70
+ const crc32 = this.readUint32LE();
71
+ const compressedSize = this.readUint32LE();
72
+ const uncompressedSize = this.readUint32LE();
73
+ const nameLength = this.readUint16LE();
74
+ const extraFieldLength = this.readUint16LE();
75
+ const commentLength = this.readUint16LE();
76
+ const diskNumber = this.readUint16LE();
77
+ const internalAttributes = this.readUint16LE();
78
+ const externalAttributes = this.readUint32LE();
79
+ const offset = this.readUint32LE();
80
+ const name = new TextDecoder().decode(this.readBuffer(nameLength));
81
+ const extraField = this.readBuffer(extraFieldLength);
82
+ const comment = this.readBuffer(commentLength);
83
+ return {
84
+ version,
85
+ requiredVersion,
86
+ flag,
87
+ compressionMethod,
88
+ lastModificationTime,
89
+ lastModificationDate,
90
+ crc32,
91
+ compressedSize,
92
+ uncompressedSize,
93
+ diskNumber,
94
+ internalAttributes,
95
+ externalAttributes,
96
+ offset,
97
+ name,
98
+ extraField,
99
+ comment,
100
+ };
101
+ }
102
+ readFileEntry(directoryEntry) {
103
+ this.seek(directoryEntry.offset);
104
+ assert(this.readUint32LE() === FILE_ENTRY_SIGNATURE);
105
+ const requiredVersion = this.readUint16LE();
106
+ assert(requiredVersion === directoryEntry.requiredVersion);
107
+ const flag = this.readUint16LE();
108
+ assert(flag === directoryEntry.flag);
109
+ const compressionMethod = this.readUint16LE();
110
+ assert(compressionMethod === directoryEntry.compressionMethod);
111
+ const lastModificationTime = this.readUint16LE();
112
+ assert(lastModificationTime === directoryEntry.lastModificationTime);
113
+ const lastModificationDate = this.readUint16LE();
114
+ assert(lastModificationDate === directoryEntry.lastModificationDate);
115
+ let crc32 = this.readUint32LE();
116
+ if (crc32 !== 0) {
117
+ assert(crc32 === directoryEntry.crc32);
118
+ }
119
+ else {
120
+ crc32 = directoryEntry.crc32;
121
+ }
122
+ let compressedSize = this.readUint32LE();
123
+ if (compressedSize !== 0) {
124
+ assert(compressedSize === directoryEntry.compressedSize);
125
+ }
126
+ else {
127
+ compressedSize = directoryEntry.compressedSize;
128
+ }
129
+ let uncompressedSize = this.readUint32LE();
130
+ if (uncompressedSize !== 0) {
131
+ assert(uncompressedSize === directoryEntry.uncompressedSize);
132
+ }
133
+ else {
134
+ uncompressedSize = directoryEntry.uncompressedSize;
135
+ }
136
+ const nameLength = this.readUint16LE();
137
+ const extraFieldLength = this.readUint16LE();
138
+ const name = new TextDecoder().decode(this.readBuffer(nameLength));
139
+ assert(name === directoryEntry.name);
140
+ const extraField = this.readBuffer(extraFieldLength);
141
+ assert(new Uint8ArrayExtension(extraField).equals(directoryEntry.extraField));
142
+ const data = this.readBuffer(compressedSize);
143
+ return {
144
+ requiredVersion,
145
+ flag,
146
+ compressionMethod,
147
+ lastModificationTime,
148
+ lastModificationDate,
149
+ crc32,
150
+ compressedSize,
151
+ uncompressedSize,
152
+ name,
153
+ extraField,
154
+ data,
155
+ };
156
+ }
157
+ }
158
+ class Writer {
159
+ #chunks = new Array();
160
+ #length = 0;
161
+ writeBuffer(buffer) {
162
+ this.#chunks.push(buffer);
163
+ this.#length += buffer.length;
164
+ return this;
165
+ }
166
+ writeUint16LE(value) {
167
+ return this.writeBuffer(Uint8ArrayExtension.fromUint16LE(value));
168
+ }
169
+ writeUint32LE(value) {
170
+ return this.writeBuffer(Uint8ArrayExtension.fromUint32LE(value));
171
+ }
172
+ get length() {
173
+ return this.#length;
174
+ }
175
+ writeDirectory(directory) {
176
+ this.writeUint32LE(DIRECTORY_SIGNATURE);
177
+ this.writeUint16LE(directory.countDisks);
178
+ this.writeUint16LE(directory.diskNumber);
179
+ this.writeUint16LE(directory.countDiskRecords);
180
+ this.writeUint16LE(directory.countRecords);
181
+ this.writeUint32LE(directory.size);
182
+ this.writeUint32LE(directory.offset);
183
+ this.writeUint16LE(directory.comment.length);
184
+ this.writeBuffer(directory.comment);
185
+ return this;
186
+ }
187
+ writeDirectoryEntry(entry) {
188
+ const encodedName = new TextEncoder().encode(entry.name);
189
+ this.writeUint32LE(DIRECTORY_ENTRY_SIGNATURE);
190
+ this.writeUint16LE(entry.version);
191
+ this.writeUint16LE(entry.requiredVersion);
192
+ this.writeUint16LE(entry.flag);
193
+ this.writeUint16LE(entry.compressionMethod);
194
+ this.writeUint16LE(entry.lastModificationTime);
195
+ this.writeUint16LE(entry.lastModificationDate);
196
+ this.writeUint32LE(entry.crc32);
197
+ this.writeUint32LE(entry.compressedSize);
198
+ this.writeUint32LE(entry.uncompressedSize);
199
+ this.writeUint16LE(encodedName.length);
200
+ this.writeUint16LE(entry.extraField.length);
201
+ this.writeUint16LE(entry.comment.length);
202
+ this.writeUint16LE(entry.diskNumber);
203
+ this.writeUint16LE(entry.internalAttributes);
204
+ this.writeUint32LE(entry.externalAttributes);
205
+ this.writeUint32LE(entry.offset);
206
+ this.writeBuffer(encodedName);
207
+ this.writeBuffer(entry.extraField);
208
+ this.writeBuffer(entry.comment);
209
+ return this;
210
+ }
211
+ writeFileEntry(entry) {
212
+ const encodedName = new TextEncoder().encode(entry.name);
213
+ this.writeUint32LE(FILE_ENTRY_SIGNATURE);
214
+ this.writeUint16LE(entry.requiredVersion);
215
+ this.writeUint16LE(entry.flag);
216
+ this.writeUint16LE(entry.compressionMethod);
217
+ this.writeUint16LE(entry.lastModificationTime);
218
+ this.writeUint16LE(entry.lastModificationDate);
219
+ this.writeUint32LE(entry.crc32);
220
+ this.writeUint32LE(entry.compressedSize);
221
+ this.writeUint32LE(entry.uncompressedSize);
222
+ this.writeUint16LE(encodedName.length);
223
+ this.writeUint16LE(entry.extraField.length);
224
+ this.writeBuffer(encodedName);
225
+ this.writeBuffer(entry.extraField);
226
+ this.writeBuffer(entry.data);
227
+ }
228
+ serialize() {
229
+ return Uint8ArrayExtension.concat(this.#chunks);
230
+ }
231
+ }
232
+ function deserializeLastModification({ lastModificationDate, lastModificationTime }) {
233
+ const year = ((lastModificationDate >> 9) & 0x7f) + 1980;
234
+ const month = (lastModificationDate >> 5) & 0xf;
235
+ const day = lastModificationDate & 0x1f;
236
+ const hour = (lastModificationTime >> 11) & 0x1f;
237
+ const minute = (lastModificationTime >> 5) & 0x3f;
238
+ const second = (lastModificationTime & 0x1f) * 2;
239
+ return new Date(year, month - 1, day, hour, minute, second);
240
+ }
241
+ function serializeLastModification(date) {
242
+ const year = date.getFullYear();
243
+ const month = date.getMonth() + 1;
244
+ const day = date.getDate();
245
+ const hour = date.getHours();
246
+ const minute = date.getMinutes();
247
+ const second = date.getSeconds();
248
+ return {
249
+ lastModificationDate: ((year - 1980) << 9) | (month << 5) | day,
250
+ lastModificationTime: (hour << 11) | (minute << 5) | (second / 2),
251
+ };
252
+ }
253
+ /**
254
+ * Extracts files from a ZIP archive.
255
+ * @param buffer An Uint8Array containing the ZIP archive.
256
+ * @returns A list of files extracted from the ZIP archive.
257
+ */
258
+ export async function extract(buffer) {
259
+ const reader = new Reader(buffer);
260
+ reader.locateDirectory();
261
+ const directory = reader.readDirectory();
262
+ assert(directory.countDisks === 0);
263
+ assert(directory.diskNumber === 0);
264
+ assert(directory.countDiskRecords === directory.countRecords);
265
+ reader.seek(directory.offset);
266
+ const directoryEntries = new Array();
267
+ for (let i = 0; i < directory.countRecords; i++) {
268
+ const directoryEntry = reader.readDirectoryEntry();
269
+ assert(directoryEntry.diskNumber === 0);
270
+ directoryEntries.push(directoryEntry);
271
+ }
272
+ const files = new Array();
273
+ for (const directoryEntry of directoryEntries) {
274
+ const fileEntry = reader.readFileEntry(directoryEntry);
275
+ const data = await (async () => {
276
+ if (fileEntry.compressionMethod === 0) {
277
+ return fileEntry.data;
278
+ }
279
+ if (fileEntry.compressionMethod === 8) {
280
+ return await decompress(fileEntry.data, true);
281
+ }
282
+ throw new Error(`Unsupported compression method ${fileEntry.compressionMethod}`);
283
+ })();
284
+ const lastModification = deserializeLastModification({
285
+ lastModificationDate: fileEntry.lastModificationDate,
286
+ lastModificationTime: fileEntry.lastModificationTime,
287
+ });
288
+ assert(fileEntry.uncompressedSize === data.length);
289
+ assert(crc32(data) === fileEntry.crc32);
290
+ files.push({ name: fileEntry.name, data, lastModification });
291
+ }
292
+ return files;
293
+ }
294
+ /**
295
+ * Creates a ZIP archive from a list of files.
296
+ * @param files List of files to include in the archive.
297
+ * @returns An Uint8Array containing the ZIP archive.
298
+ */
299
+ export async function create(files) {
300
+ const writer = new Writer();
301
+ const entries = new Array();
302
+ for (const file of files) {
303
+ const { data, compressed } = await (async () => {
304
+ const compressedData = await compress(file.data, true);
305
+ if (compressedData.length < file.data.length) {
306
+ return { data: compressedData, compressed: true };
307
+ }
308
+ else {
309
+ return { data: file.data, compressed: false };
310
+ }
311
+ })();
312
+ const { lastModificationDate, lastModificationTime } = serializeLastModification(file.lastModification ?? new Date());
313
+ const entry = {
314
+ version: 45,
315
+ requiredVersion: 20,
316
+ flag: 6,
317
+ compressionMethod: compressed ? 8 : 0,
318
+ lastModificationTime,
319
+ lastModificationDate,
320
+ crc32: crc32(file.data),
321
+ compressedSize: data.length,
322
+ uncompressedSize: file.data.length,
323
+ diskNumber: 0,
324
+ internalAttributes: 0,
325
+ externalAttributes: 0,
326
+ offset: writer.length,
327
+ name: file.name,
328
+ extraField: new Uint8Array(),
329
+ comment: new Uint8Array(),
330
+ data,
331
+ };
332
+ entries.push(entry);
333
+ writer.writeFileEntry(entry);
334
+ }
335
+ const offset = writer.length;
336
+ for (const directoryEntry of entries) {
337
+ writer.writeDirectoryEntry(directoryEntry);
338
+ }
339
+ writer.writeDirectory({
340
+ countDisks: 0,
341
+ diskNumber: 0,
342
+ countDiskRecords: entries.length,
343
+ countRecords: entries.length,
344
+ size: writer.length - offset,
345
+ offset,
346
+ comment: new Uint8Array(),
347
+ });
348
+ return writer.serialize();
349
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@quentinadam/zip",
3
+ "version": "0.1.4",
4
+ "description": "A library for creating and extracting ZIP archives",
5
+ "license": "MIT",
6
+ "author": "Quentin Adam",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/quentinadam/deno-zip.git"
10
+ },
11
+ "type": "module",
12
+ "exports": "./dist/zip.js",
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "dependencies": {
18
+ "@quentinadam/assert": "^0.1.10",
19
+ "@quentinadam/require": "^0.1.4",
20
+ "@quentinadam/uint8array-extension": "^0.1.5"
21
+ }
22
+ }