zip-writer 1.0.0
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/.claude/settings.local.json +11 -0
- package/.github/workflows/test.yml +36 -0
- package/.husky/pre-commit +1 -0
- package/eslint.config.mjs +9 -0
- package/package.json +34 -0
- package/src/constants.ts +31 -0
- package/src/crc-lookup.ts +27 -0
- package/src/crc.ts +70 -0
- package/src/index.ts +342 -0
- package/src/utils.ts +17 -0
- package/src/write-central-directory.ts +405 -0
- package/src/write-entry.ts +246 -0
- package/test/commands.ts +47 -0
- package/test/crc.test.ts +45 -0
- package/test/integration.test.ts +483 -0
- package/test/utils.ts +40 -0
- package/tsconfig.json +36 -0
- package/tsconfig.node.json +10 -0
- package/vitest.config.ts +46 -0
package/test/crc.test.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, assert } from "vitest";
|
|
2
|
+
import { crc32 } from "../src/crc.js";
|
|
3
|
+
|
|
4
|
+
describe("CRC32", () => {
|
|
5
|
+
const testString =
|
|
6
|
+
"abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh";
|
|
7
|
+
const expectedCrc = 0x6b64f5d;
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
const testBuffer = encoder.encode(testString);
|
|
10
|
+
|
|
11
|
+
it("should calculate correct CRC when writing entire buffer at once", () => {
|
|
12
|
+
const crc = crc32(testBuffer);
|
|
13
|
+
assert.strictEqual(crc, expectedCrc);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should calculate correct CRC when writing in chunks > 16 bytes", () => {
|
|
17
|
+
let crc = 0;
|
|
18
|
+
const chunkSize = 20; // > 16
|
|
19
|
+
for (let i = 0; i < testBuffer.length; i += chunkSize) {
|
|
20
|
+
const chunk = testBuffer.slice(i, i + chunkSize);
|
|
21
|
+
crc = crc32(chunk, crc);
|
|
22
|
+
}
|
|
23
|
+
assert.strictEqual(crc, expectedCrc);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should calculate correct CRC when writing in chunks < 16 bytes", () => {
|
|
27
|
+
let crc = 0;
|
|
28
|
+
const chunkSize = 10; // < 16
|
|
29
|
+
for (let i = 0; i < testBuffer.length; i += chunkSize) {
|
|
30
|
+
const chunk = testBuffer.slice(i, i + chunkSize);
|
|
31
|
+
crc = crc32(chunk, crc);
|
|
32
|
+
}
|
|
33
|
+
assert.strictEqual(crc, expectedCrc);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should calculate correct CRC when writing in chunks of exactly 16 bytes", () => {
|
|
37
|
+
let crc = 0;
|
|
38
|
+
const chunkSize = 16; // = 16
|
|
39
|
+
for (let i = 0; i < testBuffer.length; i += chunkSize) {
|
|
40
|
+
const chunk = testBuffer.slice(i, i + chunkSize);
|
|
41
|
+
crc = crc32(chunk, crc);
|
|
42
|
+
}
|
|
43
|
+
assert.strictEqual(crc, expectedCrc);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { describe, it, assert } from "vitest";
|
|
2
|
+
import { ZipWriter } from "../src/index.js";
|
|
3
|
+
import { validateZip } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper to collect a ReadableStream into a Uint8Array
|
|
7
|
+
*/
|
|
8
|
+
async function collectStream(
|
|
9
|
+
stream: ReadableStream<ArrayBufferView>
|
|
10
|
+
): Promise<Uint8Array> {
|
|
11
|
+
const chunks: Uint8Array[] = [];
|
|
12
|
+
const reader = stream.getReader();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
while (true) {
|
|
16
|
+
const { done, value } = await reader.read();
|
|
17
|
+
if (done) break;
|
|
18
|
+
chunks.push(
|
|
19
|
+
new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
} finally {
|
|
23
|
+
reader.releaseLock();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
27
|
+
const result = new Uint8Array(totalLength);
|
|
28
|
+
let offset = 0;
|
|
29
|
+
for (const chunk of chunks) {
|
|
30
|
+
result.set(chunk, offset);
|
|
31
|
+
offset += chunk.length;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Calculate SHA256 hash of data using Web Crypto API (browser) or crypto module (Node)
|
|
38
|
+
*/
|
|
39
|
+
async function sha256(data: Uint8Array): Promise<string> {
|
|
40
|
+
if (typeof window !== "undefined" && window.crypto?.subtle) {
|
|
41
|
+
// Browser: use Web Crypto API
|
|
42
|
+
const hashBuffer = await crypto.subtle.digest(
|
|
43
|
+
"SHA-256",
|
|
44
|
+
data as BufferSource
|
|
45
|
+
);
|
|
46
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
47
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
48
|
+
} else {
|
|
49
|
+
// Node: use crypto module
|
|
50
|
+
const { createHash } = await import("crypto");
|
|
51
|
+
return createHash("sha256").update(data).digest("hex");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("ZIP Integration Tests", () => {
|
|
56
|
+
describe("Single file archives", () => {
|
|
57
|
+
it("should create a valid ZIP with one stored (uncompressed) file", async () => {
|
|
58
|
+
const zipWriter = new ZipWriter();
|
|
59
|
+
const fileName = "test.txt";
|
|
60
|
+
const fileContent = "Hello, World!";
|
|
61
|
+
const fileBytes = new TextEncoder().encode(fileContent);
|
|
62
|
+
|
|
63
|
+
// Write entry
|
|
64
|
+
const entryWriter = zipWriter.entry({ name: fileName, store: true });
|
|
65
|
+
const writer = entryWriter.writable.getWriter();
|
|
66
|
+
await writer.write(fileBytes);
|
|
67
|
+
await writer.close();
|
|
68
|
+
|
|
69
|
+
// Finalize and collect ZIP
|
|
70
|
+
await zipWriter.finalize();
|
|
71
|
+
const zipBuffer = await collectStream(zipWriter.readable);
|
|
72
|
+
|
|
73
|
+
// Validate with yauzl via validateZip helper
|
|
74
|
+
const entries = await validateZip(zipBuffer);
|
|
75
|
+
|
|
76
|
+
assert.strictEqual(entries.length, 1, "Should have one entry");
|
|
77
|
+
const entry = entries[0];
|
|
78
|
+
assert.strictEqual(entry.filename, fileName, "Filename should match");
|
|
79
|
+
assert.strictEqual(
|
|
80
|
+
entry.uncompressedSize,
|
|
81
|
+
fileBytes.length,
|
|
82
|
+
"Uncompressed size should match"
|
|
83
|
+
);
|
|
84
|
+
assert.strictEqual(
|
|
85
|
+
entry.compressionMethod,
|
|
86
|
+
0,
|
|
87
|
+
"Should use store (0) compression"
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Verify content via SHA256
|
|
91
|
+
const expectedHash = await sha256(fileBytes);
|
|
92
|
+
assert.strictEqual(
|
|
93
|
+
entry.sha256,
|
|
94
|
+
expectedHash,
|
|
95
|
+
"Content SHA256 should match"
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should create a valid ZIP with one deflated (compressed) file", async () => {
|
|
100
|
+
const zipWriter = new ZipWriter();
|
|
101
|
+
const fileName = "compressed.txt";
|
|
102
|
+
const fileContent = "A".repeat(1000); // Repeating content compresses well
|
|
103
|
+
const fileBytes = new TextEncoder().encode(fileContent);
|
|
104
|
+
|
|
105
|
+
// Write entry with deflate (default)
|
|
106
|
+
const entryWriter = zipWriter.entry({ name: fileName });
|
|
107
|
+
const writer = entryWriter.writable.getWriter();
|
|
108
|
+
await writer.write(fileBytes);
|
|
109
|
+
await writer.close();
|
|
110
|
+
|
|
111
|
+
// Finalize and collect ZIP
|
|
112
|
+
await zipWriter.finalize();
|
|
113
|
+
const zipBuffer = await collectStream(zipWriter.readable);
|
|
114
|
+
|
|
115
|
+
// Validate with yauzl via validateZip helper
|
|
116
|
+
const entries = await validateZip(zipBuffer);
|
|
117
|
+
|
|
118
|
+
assert.strictEqual(entries.length, 1, "Should have one entry");
|
|
119
|
+
const entry = entries[0];
|
|
120
|
+
assert.strictEqual(entry.filename, fileName, "Filename should match");
|
|
121
|
+
assert.strictEqual(
|
|
122
|
+
entry.uncompressedSize,
|
|
123
|
+
fileBytes.length,
|
|
124
|
+
"Uncompressed size should match"
|
|
125
|
+
);
|
|
126
|
+
assert.strictEqual(
|
|
127
|
+
entry.compressionMethod,
|
|
128
|
+
8,
|
|
129
|
+
"Should use deflate (8) compression"
|
|
130
|
+
);
|
|
131
|
+
assert.isBelow(
|
|
132
|
+
entry.compressedSize,
|
|
133
|
+
entry.uncompressedSize,
|
|
134
|
+
"Compressed size should be smaller"
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Verify content via SHA256
|
|
138
|
+
const expectedHash = await sha256(fileBytes);
|
|
139
|
+
assert.strictEqual(
|
|
140
|
+
entry.sha256,
|
|
141
|
+
expectedHash,
|
|
142
|
+
"Content SHA256 should match after decompression"
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("Multiple file archives", () => {
|
|
148
|
+
it("should create a valid ZIP with multiple files", async () => {
|
|
149
|
+
const zipWriter = new ZipWriter();
|
|
150
|
+
const files = [
|
|
151
|
+
{ name: "file1.txt", content: "First file content" },
|
|
152
|
+
{ name: "file2.txt", content: "Second file content" },
|
|
153
|
+
{ name: "file3.txt", content: "Third file content" },
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
// Write all entries
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
const entryWriter = zipWriter.entry({ name: file.name, store: true });
|
|
159
|
+
const writer = entryWriter.writable.getWriter();
|
|
160
|
+
await writer.write(new TextEncoder().encode(file.content));
|
|
161
|
+
await writer.close();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Finalize and collect ZIP
|
|
165
|
+
await zipWriter.finalize();
|
|
166
|
+
const zipBuffer = await collectStream(zipWriter.readable);
|
|
167
|
+
|
|
168
|
+
// Validate with yauzl via validateZip helper
|
|
169
|
+
const entries = await validateZip(zipBuffer);
|
|
170
|
+
|
|
171
|
+
assert.strictEqual(
|
|
172
|
+
entries.length,
|
|
173
|
+
files.length,
|
|
174
|
+
`Should have ${files.length} entries`
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Verify each file
|
|
178
|
+
for (let i = 0; i < files.length; i++) {
|
|
179
|
+
const expectedFile = files[i];
|
|
180
|
+
const entry = entries[i];
|
|
181
|
+
const expectedBytes = new TextEncoder().encode(expectedFile.content);
|
|
182
|
+
|
|
183
|
+
assert.strictEqual(
|
|
184
|
+
entry.filename,
|
|
185
|
+
expectedFile.name,
|
|
186
|
+
`Entry ${i} filename should match`
|
|
187
|
+
);
|
|
188
|
+
const expectedHash = await sha256(expectedBytes);
|
|
189
|
+
assert.strictEqual(
|
|
190
|
+
entry.sha256,
|
|
191
|
+
expectedHash,
|
|
192
|
+
`Entry ${i} content should match`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should handle files with subdirectories", async () => {
|
|
198
|
+
const zipWriter = new ZipWriter();
|
|
199
|
+
const files = [
|
|
200
|
+
{ name: "root.txt", content: "Root file" },
|
|
201
|
+
{ name: "subdir/file1.txt", content: "Subdirectory file 1" },
|
|
202
|
+
{ name: "subdir/nested/file2.txt", content: "Nested file 2" },
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
// Write all entries
|
|
206
|
+
for (const file of files) {
|
|
207
|
+
const entryWriter = zipWriter.entry({ name: file.name, store: true });
|
|
208
|
+
const writer = entryWriter.writable.getWriter();
|
|
209
|
+
await writer.write(new TextEncoder().encode(file.content));
|
|
210
|
+
await writer.close();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Finalize and collect ZIP
|
|
214
|
+
await zipWriter.finalize();
|
|
215
|
+
const zipBuffer = await collectStream(zipWriter.readable);
|
|
216
|
+
|
|
217
|
+
// Validate with yauzl via validateZip helper
|
|
218
|
+
const entries = await validateZip(zipBuffer);
|
|
219
|
+
|
|
220
|
+
assert.strictEqual(
|
|
221
|
+
entries.length,
|
|
222
|
+
files.length,
|
|
223
|
+
"Should have all entries"
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Verify paths are preserved correctly
|
|
227
|
+
for (const file of files) {
|
|
228
|
+
const entry = entries.find((e) => e.filename === file.name);
|
|
229
|
+
assert.isDefined(entry, `Should find entry for ${file.name}`);
|
|
230
|
+
|
|
231
|
+
const expectedBytes = new TextEncoder().encode(file.content);
|
|
232
|
+
const expectedHash = await sha256(expectedBytes);
|
|
233
|
+
assert.strictEqual(
|
|
234
|
+
entry!.sha256,
|
|
235
|
+
expectedHash,
|
|
236
|
+
`Content for ${file.name} should match`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("File metadata", () => {
|
|
243
|
+
it("should preserve file modification dates", async () => {
|
|
244
|
+
const zipWriter = new ZipWriter();
|
|
245
|
+
const testDate = new Date("2024-06-15T12:00:00Z");
|
|
246
|
+
const fileName = "dated-file.txt";
|
|
247
|
+
|
|
248
|
+
const entryWriter = zipWriter.entry({
|
|
249
|
+
name: fileName,
|
|
250
|
+
store: true,
|
|
251
|
+
date: testDate,
|
|
252
|
+
});
|
|
253
|
+
const writer = entryWriter.writable.getWriter();
|
|
254
|
+
await writer.write(new TextEncoder().encode("Content"));
|
|
255
|
+
await writer.close();
|
|
256
|
+
|
|
257
|
+
await zipWriter.finalize();
|
|
258
|
+
const zipBuffer = await collectStream(zipWriter.readable);
|
|
259
|
+
|
|
260
|
+
// Validate with yauzl via validateZip helper
|
|
261
|
+
const entries = await validateZip(zipBuffer);
|
|
262
|
+
|
|
263
|
+
const entry = entries[0];
|
|
264
|
+
assert.strictEqual(entry.filename, fileName);
|
|
265
|
+
// Just verify that the entry exists with proper metadata
|
|
266
|
+
assert.isAbove(entry.uncompressedSize, 0, "Should have content");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should handle file comments", async () => {
|
|
270
|
+
const zipWriter = new ZipWriter();
|
|
271
|
+
const fileName = "commented.txt";
|
|
272
|
+
const fileComment = "This is a test comment";
|
|
273
|
+
|
|
274
|
+
const entryWriter = zipWriter.entry({
|
|
275
|
+
name: fileName,
|
|
276
|
+
store: true,
|
|
277
|
+
comment: fileComment,
|
|
278
|
+
});
|
|
279
|
+
const writer = entryWriter.writable.getWriter();
|
|
280
|
+
await writer.write(new TextEncoder().encode("Content with comment"));
|
|
281
|
+
await writer.close();
|
|
282
|
+
|
|
283
|
+
await zipWriter.finalize();
|
|
284
|
+
const zipBuffer = await collectStream(zipWriter.readable);
|
|
285
|
+
|
|
286
|
+
// Validate with yauzl via validateZip helper
|
|
287
|
+
const entries = await validateZip(zipBuffer);
|
|
288
|
+
|
|
289
|
+
const entry = entries[0];
|
|
290
|
+
assert.strictEqual(entry.filename, fileName);
|
|
291
|
+
// Entry should exist - comment validation depends on reader implementation
|
|
292
|
+
assert.isDefined(entry, "Entry should exist");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should handle Unix file permissions", async () => {
|
|
296
|
+
const zipWriter = new ZipWriter();
|
|
297
|
+
const fileName = "executable.sh";
|
|
298
|
+
const fileMode = 0o755; // rwxr-xr-x
|
|
299
|
+
|
|
300
|
+
const entryWriter = zipWriter.entry({
|
|
301
|
+
name: fileName,
|
|
302
|
+
store: true,
|
|
303
|
+
mode: fileMode,
|
|
304
|
+
});
|
|
305
|
+
const writer = entryWriter.writable.getWriter();
|
|
306
|
+
await writer.write(new TextEncoder().encode("#!/bin/bash\necho 'test'"));
|
|
307
|
+
await writer.close();
|
|
308
|
+
|
|
309
|
+
await zipWriter.finalize();
|
|
310
|
+
const zipBuffer = await collectStream(zipWriter.readable);
|
|
311
|
+
|
|
312
|
+
// Validate with yauzl via validateZip helper
|
|
313
|
+
const entries = await validateZip(zipBuffer);
|
|
314
|
+
|
|
315
|
+
const entry = entries[0];
|
|
316
|
+
assert.strictEqual(entry.filename, fileName);
|
|
317
|
+
|
|
318
|
+
// Extract Unix mode from external file attributes (upper 16 bits)
|
|
319
|
+
const mode = (entry.externalFileAttributes >> 16) & 0xffff;
|
|
320
|
+
assert.strictEqual(mode, fileMode, "Unix file mode should be preserved");
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe("UTF-8 filename handling", () => {
|
|
325
|
+
it("should handle UTF-8 filenames correctly", async () => {
|
|
326
|
+
const zipWriter = new ZipWriter();
|
|
327
|
+
const files = [
|
|
328
|
+
{ name: "test-文件.txt", content: "Chinese filename" },
|
|
329
|
+
{ name: "тест.txt", content: "Cyrillic filename" },
|
|
330
|
+
{ name: "δοκιμή.txt", content: "Greek filename" },
|
|
331
|
+
{ name: "テスト.txt", content: "Japanese filename" },
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
// Write all entries
|
|
335
|
+
for (const file of files) {
|
|
336
|
+
const entryWriter = zipWriter.entry({ name: file.name, store: true });
|
|
337
|
+
const writer = entryWriter.writable.getWriter();
|
|
338
|
+
await writer.write(new TextEncoder().encode(file.content));
|
|
339
|
+
await writer.close();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
await zipWriter.finalize();
|
|
343
|
+
const zipBuffer = await collectStream(zipWriter.readable);
|
|
344
|
+
|
|
345
|
+
// Validate with yauzl via validateZip helper
|
|
346
|
+
const entries = await validateZip(zipBuffer);
|
|
347
|
+
|
|
348
|
+
assert.strictEqual(
|
|
349
|
+
entries.length,
|
|
350
|
+
files.length,
|
|
351
|
+
"Should have all UTF-8 named files"
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Verify each filename and content
|
|
355
|
+
for (const file of files) {
|
|
356
|
+
const entry = entries.find((e) => e.filename === file.name);
|
|
357
|
+
assert.isDefined(entry, `Should find entry for ${file.name}`);
|
|
358
|
+
|
|
359
|
+
const expectedBytes = new TextEncoder().encode(file.content);
|
|
360
|
+
const expectedHash = await sha256(expectedBytes);
|
|
361
|
+
assert.strictEqual(
|
|
362
|
+
entry!.sha256,
|
|
363
|
+
expectedHash,
|
|
364
|
+
`Content for ${file.name} should match`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("Empty files and edge cases", () => {
|
|
371
|
+
it("should handle empty files", async () => {
|
|
372
|
+
const zipWriter = new ZipWriter();
|
|
373
|
+
const fileName = "empty.txt";
|
|
374
|
+
|
|
375
|
+
const entryWriter = zipWriter.entry({ name: fileName, store: true });
|
|
376
|
+
const writer = entryWriter.writable.getWriter();
|
|
377
|
+
await writer.close(); // Close without writing anything
|
|
378
|
+
|
|
379
|
+
await zipWriter.finalize();
|
|
380
|
+
const zipBuffer = await collectStream(zipWriter.readable);
|
|
381
|
+
|
|
382
|
+
// Validate with yauzl via validateZip helper
|
|
383
|
+
const entries = await validateZip(zipBuffer);
|
|
384
|
+
|
|
385
|
+
const entry = entries[0];
|
|
386
|
+
assert.strictEqual(entry.filename, fileName);
|
|
387
|
+
assert.strictEqual(
|
|
388
|
+
entry.uncompressedSize,
|
|
389
|
+
0,
|
|
390
|
+
"Empty file should have 0 size"
|
|
391
|
+
);
|
|
392
|
+
assert.strictEqual(
|
|
393
|
+
entry.compressedSize,
|
|
394
|
+
0,
|
|
395
|
+
"Empty file compressed size should be 0"
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
// SHA256 of empty buffer
|
|
399
|
+
const expectedHash = await sha256(new Uint8Array(0));
|
|
400
|
+
assert.strictEqual(entry.sha256, expectedHash, "Content should be empty");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("should handle very long filenames", async () => {
|
|
404
|
+
const zipWriter = new ZipWriter();
|
|
405
|
+
// ZIP format supports filenames up to 65535 bytes
|
|
406
|
+
const longName = "a".repeat(200) + "/b".repeat(200) + "/file.txt";
|
|
407
|
+
|
|
408
|
+
const entryWriter = zipWriter.entry({ name: longName, store: true });
|
|
409
|
+
const writer = entryWriter.writable.getWriter();
|
|
410
|
+
await writer.write(new TextEncoder().encode("Content"));
|
|
411
|
+
await writer.close();
|
|
412
|
+
|
|
413
|
+
await zipWriter.finalize();
|
|
414
|
+
const zipBuffer = await collectStream(zipWriter.readable);
|
|
415
|
+
|
|
416
|
+
// Validate with yauzl via validateZip helper
|
|
417
|
+
const entries = await validateZip(zipBuffer);
|
|
418
|
+
|
|
419
|
+
const entry = entries[0];
|
|
420
|
+
assert.strictEqual(
|
|
421
|
+
entry.filename,
|
|
422
|
+
longName,
|
|
423
|
+
"Long filename should be preserved"
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe("Mixed compression methods", () => {
|
|
429
|
+
it("should handle mix of stored and deflated files in same archive", async () => {
|
|
430
|
+
const zipWriter = new ZipWriter();
|
|
431
|
+
const files = [
|
|
432
|
+
{ name: "stored.txt", content: "Stored content", store: true },
|
|
433
|
+
{ name: "deflated.txt", content: "Deflated content", store: false },
|
|
434
|
+
{ name: "stored2.txt", content: "Another stored", store: true },
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
// Write all entries
|
|
438
|
+
for (const file of files) {
|
|
439
|
+
const entryWriter = zipWriter.entry({
|
|
440
|
+
name: file.name,
|
|
441
|
+
store: file.store,
|
|
442
|
+
});
|
|
443
|
+
const writer = entryWriter.writable.getWriter();
|
|
444
|
+
await writer.write(new TextEncoder().encode(file.content));
|
|
445
|
+
await writer.close();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
await zipWriter.finalize();
|
|
449
|
+
const zipBuffer = await collectStream(zipWriter.readable);
|
|
450
|
+
|
|
451
|
+
// Validate with yauzl via validateZip helper
|
|
452
|
+
const entries = await validateZip(zipBuffer);
|
|
453
|
+
|
|
454
|
+
assert.strictEqual(
|
|
455
|
+
entries.length,
|
|
456
|
+
files.length,
|
|
457
|
+
"Should have all entries"
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
// Verify compression methods
|
|
461
|
+
for (let i = 0; i < files.length; i++) {
|
|
462
|
+
const expectedFile = files[i];
|
|
463
|
+
const entry = entries[i];
|
|
464
|
+
|
|
465
|
+
assert.strictEqual(entry.filename, expectedFile.name);
|
|
466
|
+
const expectedMethod = expectedFile.store ? 0 : 8;
|
|
467
|
+
assert.strictEqual(
|
|
468
|
+
entry.compressionMethod,
|
|
469
|
+
expectedMethod,
|
|
470
|
+
`${expectedFile.name} should use correct compression method`
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const expectedBytes = new TextEncoder().encode(expectedFile.content);
|
|
474
|
+
const expectedHash = await sha256(expectedBytes);
|
|
475
|
+
assert.strictEqual(
|
|
476
|
+
entry.sha256,
|
|
477
|
+
expectedHash,
|
|
478
|
+
`Content for ${expectedFile.name} should match`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
});
|
package/test/utils.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Shim for validateZip that works in both Node and browser contexts
|
|
2
|
+
|
|
3
|
+
export interface ZipEntryInfo {
|
|
4
|
+
filename: string;
|
|
5
|
+
uncompressedSize: number;
|
|
6
|
+
compressedSize: number;
|
|
7
|
+
compressionMethod: number;
|
|
8
|
+
sha256: string;
|
|
9
|
+
isDirectory: boolean;
|
|
10
|
+
externalFileAttributes: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert Uint8Array to hex string
|
|
15
|
+
*/
|
|
16
|
+
function toHex(buffer: Uint8Array): string {
|
|
17
|
+
return Array.from(buffer)
|
|
18
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
19
|
+
.join("");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// In Node context, import the command directly
|
|
23
|
+
// In browser context, use vitest browser commands
|
|
24
|
+
export async function validateZip(
|
|
25
|
+
zipBuffer: Uint8Array
|
|
26
|
+
): Promise<ZipEntryInfo[]> {
|
|
27
|
+
const hexString = toHex(zipBuffer);
|
|
28
|
+
|
|
29
|
+
// Check if we're in a browser context
|
|
30
|
+
if (typeof window !== "undefined") {
|
|
31
|
+
// Browser: use vitest browser commands
|
|
32
|
+
const { commands } = await import("vitest/browser");
|
|
33
|
+
return commands.validateZip(hexString);
|
|
34
|
+
} else {
|
|
35
|
+
// Node: use the command directly
|
|
36
|
+
const { validateZip: validateZipCommand } = await import("./commands.js");
|
|
37
|
+
// Call the command with a mock context
|
|
38
|
+
return validateZipCommand({} as any, hexString);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Visit https://aka.ms/tsconfig to read more about this file
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
// File Layout
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
|
|
8
|
+
// Environment Settings
|
|
9
|
+
// See also https://aka.ms/tsconfig/module
|
|
10
|
+
"module": "nodenext",
|
|
11
|
+
"target": "ES2022",
|
|
12
|
+
// Set to empty to avoid loading node types automatically
|
|
13
|
+
"typeRoots": [],
|
|
14
|
+
// For nodejs:
|
|
15
|
+
// "types": ["node"],
|
|
16
|
+
// and npm install -D @types/node
|
|
17
|
+
|
|
18
|
+
// Other Outputs
|
|
19
|
+
"sourceMap": true,
|
|
20
|
+
"declaration": true,
|
|
21
|
+
"declarationMap": true,
|
|
22
|
+
|
|
23
|
+
// Stricter Typechecking Options
|
|
24
|
+
// "noUncheckedIndexedAccess": true,
|
|
25
|
+
"exactOptionalPropertyTypes": true,
|
|
26
|
+
|
|
27
|
+
// Recommended Options
|
|
28
|
+
"strict": true,
|
|
29
|
+
"verbatimModuleSyntax": true,
|
|
30
|
+
"isolatedModules": true,
|
|
31
|
+
"noUncheckedSideEffectImports": true,
|
|
32
|
+
"moduleDetection": "force",
|
|
33
|
+
"skipLibCheck": true
|
|
34
|
+
},
|
|
35
|
+
"include": ["src/**/*.ts"]
|
|
36
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import { playwright } from "@vitest/browser-playwright";
|
|
3
|
+
import type { BrowserInstanceOption } from "vitest/node";
|
|
4
|
+
import { validateZip } from "./test/commands.js";
|
|
5
|
+
|
|
6
|
+
const browserInstances: BrowserInstanceOption[] = [
|
|
7
|
+
{ browser: "chromium" },
|
|
8
|
+
{ browser: "firefox" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
if (process.platform === "darwin") {
|
|
12
|
+
browserInstances.push({ browser: "webkit" });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
test: {
|
|
17
|
+
include: ["test/**/*.test.ts"],
|
|
18
|
+
projects: [
|
|
19
|
+
{
|
|
20
|
+
test: {
|
|
21
|
+
name: "node",
|
|
22
|
+
environment: "node",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
optimizeDeps: {
|
|
27
|
+
exclude: ["yauzl-promise"],
|
|
28
|
+
},
|
|
29
|
+
test: {
|
|
30
|
+
name: "browser",
|
|
31
|
+
browser: {
|
|
32
|
+
ui: false,
|
|
33
|
+
screenshotFailures: false,
|
|
34
|
+
enabled: true,
|
|
35
|
+
headless: true,
|
|
36
|
+
provider: playwright(),
|
|
37
|
+
instances: browserInstances,
|
|
38
|
+
commands: {
|
|
39
|
+
validateZip,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
});
|