@procedures/archive 0.0.0-stage → 0.3.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/LICENSE +21 -0
  3. package/README.md +95 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/errors.d.ts +27 -0
  6. package/dist/errors.d.ts.map +1 -0
  7. package/dist/errors.js +15 -0
  8. package/dist/errors.js.map +1 -0
  9. package/dist/format.d.ts +25 -0
  10. package/dist/format.d.ts.map +1 -0
  11. package/dist/format.js +60 -0
  12. package/dist/format.js.map +1 -0
  13. package/dist/index.d.ts +8 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +10 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/module.d.ts +34 -0
  18. package/dist/module.d.ts.map +1 -0
  19. package/dist/module.js +148 -0
  20. package/dist/module.js.map +1 -0
  21. package/dist/paths.d.ts +58 -0
  22. package/dist/paths.d.ts.map +1 -0
  23. package/dist/paths.js +205 -0
  24. package/dist/paths.js.map +1 -0
  25. package/dist/procedures/archive/create.d.ts +14 -0
  26. package/dist/procedures/archive/create.d.ts.map +1 -0
  27. package/dist/procedures/archive/create.js +81 -0
  28. package/dist/procedures/archive/create.js.map +1 -0
  29. package/dist/procedures/archive/extract.d.ts +17 -0
  30. package/dist/procedures/archive/extract.d.ts.map +1 -0
  31. package/dist/procedures/archive/extract.js +101 -0
  32. package/dist/procedures/archive/extract.js.map +1 -0
  33. package/dist/procedures/archive/index.d.ts +4 -0
  34. package/dist/procedures/archive/index.d.ts.map +1 -0
  35. package/dist/procedures/archive/index.js +4 -0
  36. package/dist/procedures/archive/index.js.map +1 -0
  37. package/dist/procedures/archive/list.d.ts +15 -0
  38. package/dist/procedures/archive/list.d.ts.map +1 -0
  39. package/dist/procedures/archive/list.js +47 -0
  40. package/dist/procedures/archive/list.js.map +1 -0
  41. package/dist/register.d.ts +6 -0
  42. package/dist/register.d.ts.map +1 -0
  43. package/dist/register.js +8 -0
  44. package/dist/register.js.map +1 -0
  45. package/dist/tar.d.ts +38 -0
  46. package/dist/tar.d.ts.map +1 -0
  47. package/dist/tar.js +193 -0
  48. package/dist/tar.js.map +1 -0
  49. package/dist/types.d.ts +81 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +8 -0
  52. package/dist/types.js.map +1 -0
  53. package/dist/walk.d.ts +34 -0
  54. package/dist/walk.d.ts.map +1 -0
  55. package/dist/walk.js +67 -0
  56. package/dist/walk.js.map +1 -0
  57. package/dist/zip.d.ts +57 -0
  58. package/dist/zip.d.ts.map +1 -0
  59. package/dist/zip.js +397 -0
  60. package/dist/zip.js.map +1 -0
  61. package/package.json +77 -3
  62. package/src/errors.ts +33 -0
  63. package/src/format.ts +64 -0
  64. package/src/index.ts +9 -0
  65. package/src/module.ts +192 -0
  66. package/src/paths.ts +230 -0
  67. package/src/procedures/archive/create.ts +84 -0
  68. package/src/procedures/archive/extract.ts +103 -0
  69. package/src/procedures/archive/index.ts +3 -0
  70. package/src/procedures/archive/list.ts +50 -0
  71. package/src/register.ts +10 -0
  72. package/src/tar.ts +198 -0
  73. package/src/types.ts +101 -0
  74. package/src/walk.ts +85 -0
  75. package/src/zip.ts +419 -0
package/src/zip.ts ADDED
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Zip archives on `node:zlib` (roadmap R46).
3
+ *
4
+ * The package reads and writes zip itself, for two reasons. The reader needs the external
5
+ * attributes of each entry, to see a symbolic link, and a limit on the bytes of each entry
6
+ * against a zip bomb. A zip library that decodes in memory gives neither.
7
+ *
8
+ * - The reader takes the central directory: stored and deflated entries, UTF-8 names, and the
9
+ * zip64 fields. It refuses an encrypted entry, another method and an archive on more disks.
10
+ * - The extract streams each entry and stops when it gets more bytes than the entry declares.
11
+ * It examines the size and the CRC-32 of each entry.
12
+ * - The writer stores each file, or deflates it when that is smaller. It writes no zip64 fields:
13
+ * it refuses more than 65535 entries and an archive of 4 GiB or more.
14
+ */
15
+
16
+ import { createReadStream, createWriteStream } from "node:fs";
17
+ import { open, type FileHandle } from "node:fs/promises";
18
+ import { Readable, Transform, Writable, type TransformCallback } from "node:stream";
19
+ import { pipeline } from "node:stream/promises";
20
+ import { createInflateRaw, deflateRawSync } from "node:zlib";
21
+ import { ArchiveError } from "./errors.js";
22
+
23
+ const SIG_LOCAL = 0x04034b50;
24
+ const SIG_CENTRAL = 0x02014b50;
25
+ const SIG_END = 0x06054b50;
26
+ const SIG_END64 = 0x06064b50;
27
+ const SIG_LOCATOR64 = 0x07064b50;
28
+ const FLAG_ENCRYPTED = 0x0001;
29
+ const FLAG_UTF8 = 0x0800;
30
+ const METHOD_STORE = 0;
31
+ const METHOD_DEFLATE = 8;
32
+ const OS_UNIX = 3;
33
+ const S_IFMT = 0o170000;
34
+ const S_IFLNK = 0o120000;
35
+ const S_IFDIR = 0o040000;
36
+ const S_IFREG = 0o100000;
37
+ /** The largest central directory that the reader takes. */
38
+ const MAX_DIRECTORY_BYTES = 256 * 1024 * 1024;
39
+ /** The largest target of a symbolic link. */
40
+ const MAX_LINK_BYTES = 4096;
41
+
42
+ // =============================================================================
43
+ // CRC-32
44
+ // =============================================================================
45
+
46
+ const CRC_TABLE = (() => {
47
+ const table = new Uint32Array(256);
48
+ for (let n = 0; n < 256; n++) {
49
+ let c = n;
50
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
51
+ table[n] = c >>> 0;
52
+ }
53
+ return table;
54
+ })();
55
+
56
+ /** The CRC-32 of bytes, continued from `crc` (0 for new bytes). */
57
+ export function crc32(bytes: Uint8Array, crc = 0): number {
58
+ let c = ~crc >>> 0;
59
+ for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]!) & 0xff]! ^ (c >>> 8);
60
+ return ~c >>> 0;
61
+ }
62
+
63
+ // =============================================================================
64
+ // Reading
65
+ // =============================================================================
66
+
67
+ /** One entry of the central directory. */
68
+ export interface ZipEntry {
69
+ readonly name: string;
70
+ readonly kind: "file" | "directory" | "symlink";
71
+ readonly method: number;
72
+ readonly flags: number;
73
+ readonly crc: number;
74
+ readonly compressedSize: number;
75
+ readonly size: number;
76
+ readonly localOffset: number;
77
+ /** The mode bits of a Unix entry, without the type. */
78
+ readonly mode: number | undefined;
79
+ readonly mtime: Date;
80
+ }
81
+
82
+ /** The time of a DOS date and time, as a local time. */
83
+ function dosTime(date: number, time: number): Date {
84
+ return new Date(1980 + (date >> 9), ((date >> 5) & 15) - 1, date & 31, time >> 11, (time >> 5) & 63, (time & 31) * 2);
85
+ }
86
+
87
+ /** The DOS date and time of a time. */
88
+ function toDosTime(when: Date): { date: number; time: number } {
89
+ const year = Math.min(Math.max(when.getFullYear(), 1980), 2107);
90
+ return {
91
+ date: ((year - 1980) << 9) | ((when.getMonth() + 1) << 5) | when.getDate(),
92
+ time: (when.getHours() << 11) | (when.getMinutes() << 5) | Math.floor(when.getSeconds() / 2),
93
+ };
94
+ }
95
+
96
+ const utf8 = new TextDecoder("utf-8", { fatal: true });
97
+
98
+ /** The text of a name: UTF-8 when the bytes are UTF-8, else Latin-1. */
99
+ function decodeName(bytes: Buffer, flags: number): string {
100
+ if (flags & FLAG_UTF8) return bytes.toString("utf8");
101
+ try {
102
+ return utf8.decode(bytes);
103
+ } catch {
104
+ return bytes.toString("latin1");
105
+ }
106
+ }
107
+
108
+ /** A 64-bit number that must fit in a safe integer. */
109
+ function u64(buffer: Buffer, offset: number): number {
110
+ const value = buffer.readBigUInt64LE(offset);
111
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new ArchiveError("BAD_ARCHIVE", "The zip has a size or an offset that is too large");
112
+ return Number(value);
113
+ }
114
+
115
+ async function readAt(handle: FileHandle, offset: number, length: number): Promise<Buffer> {
116
+ const buffer = Buffer.alloc(length);
117
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
118
+ if (bytesRead !== length) throw new ArchiveError("BAD_ARCHIVE", "The zip ends before its central directory ends");
119
+ return buffer;
120
+ }
121
+
122
+ /** The place and the size of the central directory, and the number of entries. */
123
+ async function findDirectory(handle: FileHandle, fileSize: number): Promise<{ offset: number; size: number; count: number }> {
124
+ const tailLength = Math.min(fileSize, 22 + 65_535);
125
+ const tail = await readAt(handle, fileSize - tailLength, tailLength);
126
+ let at = -1;
127
+ for (let i = tail.length - 22; i >= 0; i--) {
128
+ if (tail.readUInt32LE(i) === SIG_END) {
129
+ at = i;
130
+ break;
131
+ }
132
+ }
133
+ if (at < 0) throw new ArchiveError("BAD_ARCHIVE", "The file has no end of a zip central directory");
134
+ if (tail.readUInt16LE(at + 4) !== 0 || tail.readUInt16LE(at + 6) !== 0) throw new ArchiveError("BAD_ARCHIVE", "The zip is on more than one disk");
135
+ let count = tail.readUInt16LE(at + 10);
136
+ let size = tail.readUInt32LE(at + 12);
137
+ let offset = tail.readUInt32LE(at + 16);
138
+ if (count === 0xffff || size === 0xffffffff || offset === 0xffffffff) {
139
+ const locatorAt = fileSize - tailLength + at - 20;
140
+ if (locatorAt < 0) throw new ArchiveError("BAD_ARCHIVE", "The zip64 locator is missing");
141
+ const locator = await readAt(handle, locatorAt, 20);
142
+ if (locator.readUInt32LE(0) !== SIG_LOCATOR64) throw new ArchiveError("BAD_ARCHIVE", "The zip64 locator is missing");
143
+ const end64 = await readAt(handle, u64(locator, 8), 56);
144
+ if (end64.readUInt32LE(0) !== SIG_END64) throw new ArchiveError("BAD_ARCHIVE", "The zip64 end of the central directory is missing");
145
+ count = u64(end64, 32);
146
+ size = u64(end64, 40);
147
+ offset = u64(end64, 48);
148
+ }
149
+ if (offset + size > fileSize) throw new ArchiveError("BAD_ARCHIVE", "The central directory of the zip is out of the file");
150
+ if (size > MAX_DIRECTORY_BYTES) throw new ArchiveError("TOO_LARGE", "The central directory of the zip is too large");
151
+ return { offset, size, count };
152
+ }
153
+
154
+ /** The kind of an entry: from the Unix type of its attributes, from a name with `/`, or from the DOS folder flag. */
155
+ function kindOf(name: string, madeBy: number, external: number): ZipEntry["kind"] {
156
+ const unixType = madeBy >> 8 === OS_UNIX ? (external >>> 16) & S_IFMT : 0;
157
+ if (unixType === S_IFLNK) return "symlink";
158
+ if (unixType === S_IFDIR || name.endsWith("/") || name.endsWith("\\") || (external & 0x10) !== 0) return "directory";
159
+ return "file";
160
+ }
161
+
162
+ /** This function reads the central directory of a zip. It throws `TOO_LARGE` for more than `maxEntries` entries. */
163
+ export async function readZipDirectory(path: string, maxEntries: number): Promise<ZipEntry[]> {
164
+ const handle = await open(path, "r");
165
+ try {
166
+ const { size: fileSize } = await handle.stat();
167
+ const directory = await findDirectory(handle, fileSize);
168
+ if (directory.count > maxEntries) throw new ArchiveError("TOO_LARGE", `The archive has ${directory.count} entries, more than the limit of ${maxEntries}`);
169
+ const buffer = await readAt(handle, directory.offset, directory.size);
170
+ const entries: ZipEntry[] = [];
171
+ let at = 0;
172
+ for (let index = 0; index < directory.count; index++) {
173
+ if (at + 46 > buffer.length || buffer.readUInt32LE(at) !== SIG_CENTRAL) throw new ArchiveError("BAD_ARCHIVE", "The central directory of the zip is damaged");
174
+ const madeBy = buffer.readUInt16LE(at + 4);
175
+ const flags = buffer.readUInt16LE(at + 8);
176
+ const method = buffer.readUInt16LE(at + 10);
177
+ const time = buffer.readUInt16LE(at + 12);
178
+ const date = buffer.readUInt16LE(at + 14);
179
+ const crc = buffer.readUInt32LE(at + 16);
180
+ let compressedSize = buffer.readUInt32LE(at + 20);
181
+ let size = buffer.readUInt32LE(at + 24);
182
+ const nameLength = buffer.readUInt16LE(at + 28);
183
+ const extraLength = buffer.readUInt16LE(at + 30);
184
+ const commentLength = buffer.readUInt16LE(at + 32);
185
+ const external = buffer.readUInt32LE(at + 38);
186
+ let localOffset = buffer.readUInt32LE(at + 42);
187
+ const nameStart = at + 46;
188
+ const extraStart = nameStart + nameLength;
189
+ const next = extraStart + extraLength + commentLength;
190
+ if (next > buffer.length) throw new ArchiveError("BAD_ARCHIVE", "The central directory of the zip is damaged");
191
+ const name = decodeName(buffer.subarray(nameStart, extraStart), flags);
192
+ // The zip64 field has the values that do not fit in 32 bits, in this order
193
+ if (size === 0xffffffff || compressedSize === 0xffffffff || localOffset === 0xffffffff) {
194
+ let field = extraStart;
195
+ let found = false;
196
+ while (field + 4 <= extraStart + extraLength) {
197
+ const id = buffer.readUInt16LE(field);
198
+ const length = buffer.readUInt16LE(field + 2);
199
+ if (id === 0x0001) {
200
+ let value = field + 4;
201
+ const end = value + length;
202
+ const take = (): number => {
203
+ if (value + 8 > end) throw new ArchiveError("BAD_ARCHIVE", `The zip64 field of ${name} is too short`, name);
204
+ const result = u64(buffer, value);
205
+ value += 8;
206
+ return result;
207
+ };
208
+ if (size === 0xffffffff) size = take();
209
+ if (compressedSize === 0xffffffff) compressedSize = take();
210
+ if (localOffset === 0xffffffff) localOffset = take();
211
+ found = true;
212
+ break;
213
+ }
214
+ field += 4 + length;
215
+ }
216
+ if (!found) throw new ArchiveError("BAD_ARCHIVE", `The entry ${name} has no zip64 field`, name);
217
+ }
218
+ const unixMode = madeBy >> 8 === OS_UNIX ? (external >>> 16) & 0o7777 : 0;
219
+ entries.push({
220
+ name,
221
+ kind: kindOf(name, madeBy, external),
222
+ method,
223
+ flags,
224
+ crc,
225
+ compressedSize,
226
+ size,
227
+ localOffset,
228
+ mode: unixMode === 0 ? undefined : unixMode,
229
+ mtime: dosTime(date, time),
230
+ });
231
+ at = next;
232
+ }
233
+ return entries;
234
+ } finally {
235
+ await handle.close();
236
+ }
237
+ }
238
+
239
+ /** The offset of the data of an entry: after its local header. */
240
+ async function localDataOffset(archive: string, entry: ZipEntry): Promise<number> {
241
+ const handle = await open(archive, "r");
242
+ try {
243
+ const header = await readAt(handle, entry.localOffset, 30);
244
+ if (header.readUInt32LE(0) !== SIG_LOCAL) throw new ArchiveError("BAD_ARCHIVE", `The local header of ${entry.name} is damaged`, entry.name);
245
+ return entry.localOffset + 30 + header.readUInt16LE(26) + header.readUInt16LE(28);
246
+ } finally {
247
+ await handle.close();
248
+ }
249
+ }
250
+
251
+ /** This function refuses an entry that the reader does not decode. */
252
+ function assertReadable(entry: ZipEntry): void {
253
+ if (entry.flags & FLAG_ENCRYPTED) throw new ArchiveError("BAD_ARCHIVE", `The entry ${entry.name} is encrypted`, entry.name);
254
+ if (entry.method !== METHOD_STORE && entry.method !== METHOD_DEFLATE) {
255
+ throw new ArchiveError("BAD_ARCHIVE", `The entry ${entry.name} uses the compression method ${entry.method}: only store and deflate are supported`, entry.name);
256
+ }
257
+ }
258
+
259
+ /** A stream step that counts the bytes and the CRC-32, and stops at more bytes than the entry declares. */
260
+ class EntryCheck extends Transform {
261
+ private bytes = 0;
262
+ private crc = 0;
263
+
264
+ constructor(private readonly entry: ZipEntry) {
265
+ super();
266
+ }
267
+
268
+ override _transform(chunk: Buffer, _encoding: BufferEncoding, done: TransformCallback): void {
269
+ this.bytes += chunk.length;
270
+ if (this.bytes > this.entry.size) {
271
+ done(new ArchiveError("BAD_ARCHIVE", `The entry ${this.entry.name} has more bytes than its declared size of ${this.entry.size}`, this.entry.name));
272
+ return;
273
+ }
274
+ this.crc = crc32(chunk, this.crc);
275
+ done(null, chunk);
276
+ }
277
+
278
+ override _flush(done: TransformCallback): void {
279
+ if (this.bytes !== this.entry.size) {
280
+ done(new ArchiveError("BAD_ARCHIVE", `The entry ${this.entry.name} has ${this.bytes} bytes, not ${this.entry.size}`, this.entry.name));
281
+ } else if (this.crc !== this.entry.crc) {
282
+ done(new ArchiveError("BAD_ARCHIVE", `The CRC-32 of the entry ${this.entry.name} is not correct`, this.entry.name));
283
+ } else {
284
+ done();
285
+ }
286
+ }
287
+ }
288
+
289
+ /**
290
+ * The decoded bytes of an entry, through the checks, into the stream of `open`. The stream opens
291
+ * after the checks of the entry and of its local header: a refused entry makes no file.
292
+ */
293
+ async function decodeEntry(archive: string, entry: ZipEntry, open: () => Writable): Promise<void> {
294
+ assertReadable(entry);
295
+ const start = await localDataOffset(archive, entry);
296
+ const destination = open();
297
+ if (entry.compressedSize === 0) {
298
+ await pipeline(Readable.from([]), new EntryCheck(entry), destination);
299
+ return;
300
+ }
301
+ const input = createReadStream(archive, { start, end: start + entry.compressedSize - 1 });
302
+ if (entry.method === METHOD_DEFLATE) {
303
+ await pipeline(input, createInflateRaw(), new EntryCheck(entry), destination);
304
+ } else {
305
+ await pipeline(input, new EntryCheck(entry), destination);
306
+ }
307
+ }
308
+
309
+ /**
310
+ * This function writes the content of a file entry to `destination`, with the checks. The file
311
+ * must not exist: the caller removes an old file first, so the write never follows a link.
312
+ */
313
+ export async function extractZipFile(archive: string, entry: ZipEntry, destination: string, mode?: number): Promise<void> {
314
+ await decodeEntry(archive, entry, () => createWriteStream(destination, { flags: "wx", ...(mode !== undefined ? { mode } : {}) }));
315
+ }
316
+
317
+ /** This function gives the target of a symbolic link entry: its content, as text. */
318
+ export async function readZipLink(archive: string, entry: ZipEntry): Promise<string> {
319
+ if (entry.size > MAX_LINK_BYTES) throw new ArchiveError("BAD_ARCHIVE", `The link ${entry.name} has a target of more than ${MAX_LINK_BYTES} bytes`, entry.name);
320
+ const chunks: Buffer[] = [];
321
+ const collect = new Writable({
322
+ write(chunk: Buffer, _encoding, done) {
323
+ chunks.push(chunk);
324
+ done();
325
+ },
326
+ });
327
+ await decodeEntry(archive, entry, () => collect);
328
+ return Buffer.concat(chunks).toString("utf8");
329
+ }
330
+
331
+ // =============================================================================
332
+ // Writing
333
+ // =============================================================================
334
+
335
+ /** One entry for the writer. */
336
+ export interface ZipWriteEntry {
337
+ /** The name in the archive. A folder ends with `/`: the writer adds it. */
338
+ readonly name: string;
339
+ readonly kind: "file" | "directory" | "symlink";
340
+ /** The content of a file, or the target of a link as UTF-8. A function loads it when the writer needs it. */
341
+ readonly data?: Uint8Array | (() => Promise<Uint8Array>) | undefined;
342
+ /** The mode bits, without the type. */
343
+ readonly mode?: number | undefined;
344
+ readonly mtime?: Date | undefined;
345
+ }
346
+
347
+ /**
348
+ * This function writes a zip to an open file. The names are not examined here: `archive.create`
349
+ * gives only the relative paths of a walk, and a test gives hostile names on purpose.
350
+ */
351
+ export async function writeZip(handle: FileHandle, entries: readonly ZipWriteEntry[]): Promise<number> {
352
+ if (entries.length > 0xffff) throw new ArchiveError("TOO_LARGE", "A zip without zip64 has at most 65535 entries");
353
+ const central: Buffer[] = [];
354
+ let offset = 0;
355
+ const write = async (buffer: Buffer): Promise<void> => {
356
+ if (offset + buffer.length >= 0xffffffff) throw new ArchiveError("TOO_LARGE", "A zip without zip64 must be smaller than 4 GiB");
357
+ await handle.write(buffer, 0, buffer.length, offset);
358
+ offset += buffer.length;
359
+ };
360
+ for (const entry of entries) {
361
+ const name = Buffer.from(entry.kind === "directory" && !entry.name.endsWith("/") ? `${entry.name}/` : entry.name, "utf8");
362
+ const raw = Buffer.from(typeof entry.data === "function" ? await entry.data() : (entry.data ?? new Uint8Array(0)));
363
+ const crc = crc32(raw);
364
+ let method = METHOD_STORE;
365
+ let stored = raw;
366
+ if (entry.kind === "file" && raw.length > 0) {
367
+ const deflated = deflateRawSync(raw);
368
+ if (deflated.length < raw.length) {
369
+ method = METHOD_DEFLATE;
370
+ stored = deflated;
371
+ }
372
+ }
373
+ const { date, time } = toDosTime(entry.mtime ?? new Date());
374
+ const type = entry.kind === "symlink" ? S_IFLNK : entry.kind === "directory" ? S_IFDIR : S_IFREG;
375
+ const mode = entry.mode ?? (entry.kind === "directory" ? 0o755 : entry.kind === "symlink" ? 0o777 : 0o644);
376
+ const external = (((type | (mode & 0o7777)) << 16) | (entry.kind === "directory" ? 0x10 : 0)) >>> 0;
377
+ const local = Buffer.alloc(30);
378
+ local.writeUInt32LE(SIG_LOCAL, 0);
379
+ local.writeUInt16LE(20, 4);
380
+ local.writeUInt16LE(FLAG_UTF8, 6);
381
+ local.writeUInt16LE(method, 8);
382
+ local.writeUInt16LE(time, 10);
383
+ local.writeUInt16LE(date, 12);
384
+ local.writeUInt32LE(crc, 14);
385
+ local.writeUInt32LE(stored.length, 18);
386
+ local.writeUInt32LE(raw.length, 22);
387
+ local.writeUInt16LE(name.length, 26);
388
+ local.writeUInt16LE(0, 28);
389
+ const localOffset = offset;
390
+ await write(Buffer.concat([local, name]));
391
+ if (stored.length > 0) await write(stored);
392
+ const record = Buffer.alloc(46);
393
+ record.writeUInt32LE(SIG_CENTRAL, 0);
394
+ record.writeUInt16LE((OS_UNIX << 8) | 20, 4);
395
+ record.writeUInt16LE(20, 6);
396
+ record.writeUInt16LE(FLAG_UTF8, 8);
397
+ record.writeUInt16LE(method, 10);
398
+ record.writeUInt16LE(time, 12);
399
+ record.writeUInt16LE(date, 14);
400
+ record.writeUInt32LE(crc, 16);
401
+ record.writeUInt32LE(stored.length, 20);
402
+ record.writeUInt32LE(raw.length, 24);
403
+ record.writeUInt16LE(name.length, 28);
404
+ record.writeUInt32LE(external, 38);
405
+ record.writeUInt32LE(localOffset, 42);
406
+ central.push(record, name);
407
+ }
408
+ const directory = Buffer.concat(central);
409
+ const directoryOffset = offset;
410
+ await write(directory);
411
+ const end = Buffer.alloc(22);
412
+ end.writeUInt32LE(SIG_END, 0);
413
+ end.writeUInt16LE(entries.length, 8);
414
+ end.writeUInt16LE(entries.length, 10);
415
+ end.writeUInt32LE(directory.length, 12);
416
+ end.writeUInt32LE(directoryOffset, 16);
417
+ await write(end);
418
+ return offset;
419
+ }