tar-vern 0.3.0 → 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/dist/index.js CHANGED
@@ -1,24 +1,30 @@
1
1
  /*!
2
2
  * name: tar-vern
3
- * version: 0.3.0
3
+ * version: 1.0.0
4
4
  * description: Tape archiver library for Typescript
5
5
  * author: Kouji Matsui (@kekyo@mi.kekyo.net)
6
6
  * license: MIT
7
7
  * repository.url: https://github.com/kekyo/tar-vern.git
8
+ * git.commit.hash: def794cc361e3023c973a6dc7719d38fc08ac363
8
9
  */
9
10
  import { Readable } from "stream";
10
- import { createGzip } from "zlib";
11
+ import { createGzip, createGunzip } from "zlib";
11
12
  import { createReadStream, createWriteStream } from "fs";
12
- import { stat } from "fs/promises";
13
+ import { stat, mkdir, writeFile } from "fs/promises";
14
+ import { pipeline } from "stream/promises";
15
+ import { join, dirname } from "path";
16
+ const MAX_NAME = 100;
17
+ const MAX_PREFIX = 155;
13
18
  const getUName = (candidateName, candidateId, reflectStat) => {
14
19
  return candidateName ?? (reflectStat === "all" ? candidateId.toString() : "root");
15
20
  };
16
21
  const getBuffer = (data) => {
17
22
  return Buffer.isBuffer(data) ? data : Buffer.from(data, "utf8");
18
23
  };
19
- const createDirectoryItem = async (path, reflectStat, options) => {
24
+ const createDirectoryItem = async (path, reflectStat, options, signal) => {
20
25
  const rs = reflectStat ?? "none";
21
26
  if (rs !== "none" && options?.directoryPath) {
27
+ signal?.throwIfAborted();
22
28
  const stats = await stat(options.directoryPath);
23
29
  const mode = options?.mode ?? stats.mode;
24
30
  const uid = options?.uid ?? stats.uid;
@@ -55,7 +61,8 @@ const createDirectoryItem = async (path, reflectStat, options) => {
55
61
  };
56
62
  }
57
63
  };
58
- const createFileItem = async (path, content, options) => {
64
+ const createFileItem = async (path, content, options, signal) => {
65
+ signal?.throwIfAborted();
59
66
  const mode = options?.mode ?? 420;
60
67
  const uid = options?.uid ?? 0;
61
68
  const gid = options?.gid ?? 0;
@@ -74,7 +81,7 @@ const createFileItem = async (path, content, options) => {
74
81
  content
75
82
  };
76
83
  };
77
- const createReadableFileItem = async (path, readable, options) => {
84
+ const createReadableFileItem = async (path, readable, options, signal) => {
78
85
  const mode = options?.mode ?? 420;
79
86
  const uid = options?.uid ?? 0;
80
87
  const gid = options?.gid ?? 0;
@@ -86,6 +93,7 @@ const createReadableFileItem = async (path, readable, options) => {
86
93
  const chunks = [];
87
94
  length = 0;
88
95
  for await (const chunk of readable) {
96
+ signal?.throwIfAborted();
89
97
  const buffer = getBuffer(chunk);
90
98
  chunks.push(buffer);
91
99
  length += buffer.length;
@@ -102,7 +110,7 @@ const createReadableFileItem = async (path, readable, options) => {
102
110
  content: {
103
111
  kind: "readable",
104
112
  length,
105
- readable: Readable.from(chunks)
113
+ readable: Readable.from(chunks, { signal })
106
114
  }
107
115
  };
108
116
  } else {
@@ -123,7 +131,7 @@ const createReadableFileItem = async (path, readable, options) => {
123
131
  };
124
132
  }
125
133
  };
126
- const createGeneratorFileItem = async (path, generator, options) => {
134
+ const createGeneratorFileItem = async (path, generator, options, signal) => {
127
135
  const mode = options?.mode ?? 420;
128
136
  const uid = options?.uid ?? 0;
129
137
  const gid = options?.gid ?? 0;
@@ -135,6 +143,7 @@ const createGeneratorFileItem = async (path, generator, options) => {
135
143
  const chunks = [];
136
144
  length = 0;
137
145
  for await (const chunk of generator) {
146
+ signal?.throwIfAborted();
138
147
  const buffer = getBuffer(chunk);
139
148
  chunks.push(buffer);
140
149
  length += buffer.length;
@@ -151,7 +160,7 @@ const createGeneratorFileItem = async (path, generator, options) => {
151
160
  content: {
152
161
  kind: "readable",
153
162
  length,
154
- readable: Readable.from(chunks)
163
+ readable: Readable.from(chunks, { signal })
155
164
  }
156
165
  };
157
166
  } else {
@@ -172,10 +181,11 @@ const createGeneratorFileItem = async (path, generator, options) => {
172
181
  };
173
182
  }
174
183
  };
175
- const createReadFileItem = async (path, filePath, reflectStat, options) => {
184
+ const createReadFileItem = async (path, filePath, reflectStat, options, signal) => {
176
185
  const rs = reflectStat ?? "exceptName";
186
+ signal?.throwIfAborted();
177
187
  const stats = await stat(filePath);
178
- const reader = createReadStream(filePath);
188
+ const reader = createReadStream(filePath, { signal });
179
189
  const mode = options?.mode ?? (rs !== "none" ? stats.mode : void 0);
180
190
  const uid = options?.uid ?? (rs !== "none" ? stats.uid : void 0);
181
191
  const gid = options?.gid ?? (rs !== "none" ? stats.gid : void 0);
@@ -190,16 +200,55 @@ const createReadFileItem = async (path, filePath, reflectStat, options) => {
190
200
  uid,
191
201
  gid,
192
202
  date
193
- });
203
+ }, signal);
204
+ };
205
+ const storeReaderToFile = async (reader, path, signal) => {
206
+ const writer = createWriteStream(path, { signal });
207
+ await pipeline(reader, writer, { signal });
208
+ };
209
+ const createEntryItemGenerator = async function* (baseDir, relativePaths, reflectStat, signal) {
210
+ const rs = reflectStat ?? "exceptName";
211
+ for (const relativePath of relativePaths) {
212
+ signal?.throwIfAborted();
213
+ const fsPath = join(baseDir, relativePath);
214
+ try {
215
+ signal?.throwIfAborted();
216
+ const stats = await stat(fsPath);
217
+ if (stats.isDirectory()) {
218
+ yield await createDirectoryItem(relativePath, rs, {
219
+ directoryPath: fsPath
220
+ }, signal);
221
+ } else if (stats.isFile()) {
222
+ yield await createReadFileItem(relativePath, fsPath, rs, void 0, signal);
223
+ }
224
+ } catch (error) {
225
+ console.warn(`Warning: Could not access ${fsPath}:`, error);
226
+ continue;
227
+ }
228
+ }
194
229
  };
195
- const storeReaderToFile = (reader, path) => {
196
- const writer = createWriteStream(path);
197
- reader.pipe(writer);
198
- return new Promise((res, rej) => {
199
- writer.on("finish", res);
200
- writer.on("error", rej);
201
- reader.on("error", rej);
202
- });
230
+ const extractTo = async (iterator, basePath, signal) => {
231
+ for await (const entry of iterator) {
232
+ signal?.throwIfAborted();
233
+ const targetPath = join(basePath, entry.path);
234
+ if (entry.kind === "directory") {
235
+ try {
236
+ signal?.throwIfAborted();
237
+ await mkdir(targetPath, { recursive: true, mode: entry.mode });
238
+ } catch (error) {
239
+ if (error.code !== "EEXIST") {
240
+ throw error;
241
+ }
242
+ }
243
+ } else if (entry.kind === "file") {
244
+ const parentDir = dirname(targetPath);
245
+ signal?.throwIfAborted();
246
+ await mkdir(parentDir, { recursive: true });
247
+ const fileEntry = entry;
248
+ const content = await fileEntry.getContent("buffer");
249
+ await writeFile(targetPath, content, { mode: entry.mode, signal });
250
+ }
251
+ }
203
252
  };
204
253
  const utf8ByteLength = (str) => {
205
254
  return Buffer.byteLength(str, "utf8");
@@ -217,8 +266,6 @@ const truncateUtf8Safe = (str, maxBytes) => {
217
266
  }
218
267
  return str.slice(0, i);
219
268
  };
220
- const MAX_NAME = 100;
221
- const MAX_PREFIX = 155;
222
269
  const splitPath = (path) => {
223
270
  if (utf8ByteLength(path) <= MAX_NAME) {
224
271
  return { prefix: "", name: path };
@@ -299,10 +346,11 @@ const createTarPacker = (entryItemGenerator, compressionType, signal) => {
299
346
  const totalPaddedContentBytes = getPaddedBytes(contentBytes);
300
347
  yield totalPaddedContentBytes;
301
348
  } else {
349
+ const content = entryItemContent;
302
350
  const tarHeaderBytes = createTarHeader(
303
351
  "file",
304
352
  entryItem.path,
305
- entryItemContent.length,
353
+ content.length,
306
354
  entryItem.mode,
307
355
  entryItem.uname,
308
356
  entryItem.gname,
@@ -312,10 +360,10 @@ const createTarPacker = (entryItemGenerator, compressionType, signal) => {
312
360
  );
313
361
  yield tarHeaderBytes;
314
362
  let position = 0;
315
- switch (entryItemContent.kind) {
363
+ switch (content.kind) {
316
364
  // Content is a generator
317
365
  case "generator": {
318
- for await (const contentBytes of entryItemContent.generator) {
366
+ for await (const contentBytes of content.generator) {
319
367
  signal?.throwIfAborted();
320
368
  yield contentBytes;
321
369
  position += contentBytes.length;
@@ -324,9 +372,9 @@ const createTarPacker = (entryItemGenerator, compressionType, signal) => {
324
372
  }
325
373
  // Content is a readable stream
326
374
  case "readable": {
327
- for await (const content of entryItemContent.readable) {
375
+ for await (const chunk of content.readable) {
328
376
  signal?.throwIfAborted();
329
- const contentBytes = getBuffer(content);
377
+ const contentBytes = getBuffer(chunk);
330
378
  yield contentBytes;
331
379
  position += contentBytes.length;
332
380
  }
@@ -364,25 +412,282 @@ const createTarPacker = (entryItemGenerator, compressionType, signal) => {
364
412
  switch (ct) {
365
413
  // No compression
366
414
  case "none": {
367
- return Readable.from(entryItemIterator());
415
+ return Readable.from(entryItemIterator(), { signal });
368
416
  }
369
417
  // Gzip compression
370
418
  case "gzip": {
371
419
  const gzipStream = createGzip({ level: 9 });
372
- const entryItemStream = Readable.from(entryItemIterator());
420
+ const entryItemStream = Readable.from(entryItemIterator(), { signal });
373
421
  entryItemStream.pipe(gzipStream);
374
422
  return gzipStream;
375
423
  }
376
424
  }
377
425
  };
426
+ const parseOctalBytes = (buffer, offset, length) => {
427
+ const str = buffer.subarray(offset, offset + length).toString("ascii").replace(/\0/g, "").trim();
428
+ return str ? parseInt(str, 8) : 0;
429
+ };
430
+ const parseString = (buffer, offset, length) => {
431
+ return buffer.subarray(offset, offset + length).toString("utf8").replace(/\0/g, "").trim();
432
+ };
433
+ const readExactBytes = async (iterator, size, signal) => {
434
+ const chunks = [];
435
+ let totalRead = 0;
436
+ while (totalRead < size) {
437
+ signal?.throwIfAborted();
438
+ const { value, done } = await iterator.next();
439
+ if (done) {
440
+ if (totalRead === 0) {
441
+ return void 0;
442
+ } else {
443
+ throw new Error(`Unexpected end of stream: expected ${size} bytes, got ${totalRead} bytes`);
444
+ }
445
+ }
446
+ const chunk = getBuffer(value);
447
+ const needed = size - totalRead;
448
+ if (chunk.length <= needed) {
449
+ chunks.push(chunk);
450
+ totalRead += chunk.length;
451
+ } else {
452
+ chunks.push(chunk.subarray(0, needed));
453
+ await iterator.return?.(chunk.subarray(needed));
454
+ totalRead = size;
455
+ }
456
+ }
457
+ return Buffer.concat(chunks, size);
458
+ };
459
+ const skipExactBytes = async (iterator, size, signal) => {
460
+ let totalSkipped = 0;
461
+ while (totalSkipped < size) {
462
+ signal?.throwIfAborted();
463
+ const { value, done } = await iterator.next();
464
+ if (done) {
465
+ throw new Error(`Unexpected end of stream: expected to skip ${size} bytes, skipped ${totalSkipped} bytes`);
466
+ }
467
+ const chunk = getBuffer(value);
468
+ const needed = size - totalSkipped;
469
+ if (chunk.length <= needed) {
470
+ totalSkipped += chunk.length;
471
+ } else {
472
+ await iterator.return?.(chunk.subarray(needed));
473
+ totalSkipped = size;
474
+ }
475
+ }
476
+ };
477
+ const skipPaddingBytesTo512Boundary = async (iterator, contentSize, signal) => {
478
+ const padding = (512 - contentSize % 512) % 512;
479
+ if (padding > 0) {
480
+ await skipExactBytes(iterator, padding, signal);
481
+ }
482
+ };
483
+ const parseTarHeader = (buffer) => {
484
+ if (buffer.every((b) => b === 0)) {
485
+ return void 0;
486
+ }
487
+ const name = parseString(buffer, 0, 100);
488
+ const mode = parseOctalBytes(buffer, 100, 8);
489
+ const uid = parseOctalBytes(buffer, 108, 8);
490
+ const gid = parseOctalBytes(buffer, 116, 8);
491
+ const size = parseOctalBytes(buffer, 124, 12);
492
+ const mtime = new Date(parseOctalBytes(buffer, 136, 12) * 1e3);
493
+ const checksum = parseOctalBytes(buffer, 148, 8);
494
+ const typeflag = parseString(buffer, 156, 1);
495
+ const magic = parseString(buffer, 257, 6);
496
+ const uname = parseString(buffer, 265, 32);
497
+ const gname = parseString(buffer, 297, 32);
498
+ const prefix = parseString(buffer, 345, 155);
499
+ if (magic !== "ustar") {
500
+ throw new Error(`Invalid tar format: magic="${magic}"`);
501
+ }
502
+ let calculatedSum = 0;
503
+ for (let i = 0; i < 512; i++) {
504
+ if (i >= 148 && i < 156) {
505
+ calculatedSum += 32;
506
+ } else {
507
+ calculatedSum += buffer[i];
508
+ }
509
+ }
510
+ if (calculatedSum !== checksum) {
511
+ throw new Error(`Invalid checksum: expected ${checksum}, got ${calculatedSum}`);
512
+ }
513
+ let path = prefix ? `${prefix}/${name}` : name;
514
+ if (path.endsWith("/")) {
515
+ path = path.slice(0, -1);
516
+ }
517
+ const kind = typeflag === "5" ? "directory" : "file";
518
+ return {
519
+ kind,
520
+ path,
521
+ size,
522
+ mode,
523
+ uid,
524
+ gid,
525
+ mtime,
526
+ uname: uname || uid.toString(),
527
+ gname: gname || gid.toString(),
528
+ checksum,
529
+ consumed: false
530
+ };
531
+ };
532
+ const createBufferedAsyncIterator = (iterable, signal) => {
533
+ const buffer = [];
534
+ const iterator = iterable[Symbol.asyncIterator]();
535
+ return {
536
+ next: async () => {
537
+ signal?.throwIfAborted();
538
+ if (buffer.length > 0) {
539
+ return { value: buffer.shift(), done: false };
540
+ }
541
+ return iterator.next();
542
+ },
543
+ return: async (value) => {
544
+ if (value !== void 0) {
545
+ buffer.unshift(value);
546
+ }
547
+ return { value: void 0, done: false };
548
+ }
549
+ };
550
+ };
551
+ const createReadableFromIterator = (iterator, size, signal, consumedRef) => {
552
+ const generator = async function* () {
553
+ let remainingBytes = size;
554
+ while (remainingBytes > 0) {
555
+ signal?.throwIfAborted();
556
+ const { value, done } = await iterator.next();
557
+ if (done) {
558
+ throw new Error(`Unexpected end of stream: expected ${size} bytes, remaining ${remainingBytes} bytes`);
559
+ }
560
+ const chunk = getBuffer(value);
561
+ if (chunk.length <= remainingBytes) {
562
+ remainingBytes -= chunk.length;
563
+ yield chunk;
564
+ } else {
565
+ const needed = chunk.subarray(0, remainingBytes);
566
+ const excess = chunk.subarray(remainingBytes);
567
+ remainingBytes = 0;
568
+ await iterator.return?.(excess);
569
+ yield needed;
570
+ break;
571
+ }
572
+ }
573
+ await skipPaddingBytesTo512Boundary(iterator, size, signal);
574
+ consumedRef.consumed = true;
575
+ };
576
+ return Readable.from(generator(), { signal });
577
+ };
578
+ const createTarExtractor = async function* (readable, compressionType, signal) {
579
+ const ct = compressionType ?? "none";
580
+ let inputStream;
581
+ switch (ct) {
582
+ case "gzip":
583
+ const gunzip = createGunzip();
584
+ readable.pipe(gunzip);
585
+ inputStream = gunzip;
586
+ break;
587
+ case "none":
588
+ default:
589
+ inputStream = readable;
590
+ break;
591
+ }
592
+ const iterator = createBufferedAsyncIterator(inputStream, signal);
593
+ let header;
594
+ while (true) {
595
+ signal?.throwIfAborted();
596
+ if (header?.kind === "file" && !header.consumed) {
597
+ await skipExactBytes(iterator, header.size, signal);
598
+ await skipPaddingBytesTo512Boundary(iterator, header.size, signal);
599
+ header.consumed = true;
600
+ }
601
+ let headerBuffer;
602
+ try {
603
+ headerBuffer = await readExactBytes(iterator, 512, signal);
604
+ } catch (error) {
605
+ if (error instanceof Error && error.message.includes("Unexpected end of stream")) {
606
+ throw new Error("Invalid tar format: incomplete header");
607
+ }
608
+ throw error;
609
+ }
610
+ if (headerBuffer === void 0) {
611
+ break;
612
+ }
613
+ header = parseTarHeader(headerBuffer);
614
+ if (!header) {
615
+ const secondBlock = await readExactBytes(iterator, 512, signal);
616
+ if (secondBlock === void 0 || secondBlock.every((b) => b === 0)) {
617
+ break;
618
+ }
619
+ throw new Error("Invalid tar format: expected terminator block");
620
+ }
621
+ if (header.kind === "directory") {
622
+ yield {
623
+ kind: "directory",
624
+ path: header.path,
625
+ mode: header.mode,
626
+ uid: header.uid,
627
+ gid: header.gid,
628
+ uname: header.uname,
629
+ gname: header.gname,
630
+ date: header.mtime
631
+ };
632
+ } else {
633
+ const currentHeader = header;
634
+ yield {
635
+ kind: "file",
636
+ path: currentHeader.path,
637
+ mode: currentHeader.mode,
638
+ uid: currentHeader.uid,
639
+ gid: currentHeader.gid,
640
+ uname: currentHeader.uname,
641
+ gname: currentHeader.gname,
642
+ date: currentHeader.mtime,
643
+ getContent: async (type) => {
644
+ if (currentHeader.consumed) {
645
+ throw new Error("Content has already been consumed. Multiple calls to getContent are not supported.");
646
+ }
647
+ switch (type) {
648
+ // For string
649
+ case "string": {
650
+ const dataBuffer = await readExactBytes(iterator, currentHeader.size, signal);
651
+ if (dataBuffer === void 0) {
652
+ throw new Error(`Unexpected end of stream while reading file data for ${currentHeader.path}`);
653
+ }
654
+ await skipPaddingBytesTo512Boundary(iterator, currentHeader.size, signal);
655
+ currentHeader.consumed = true;
656
+ return dataBuffer.toString("utf8");
657
+ }
658
+ // For buffer
659
+ case "buffer": {
660
+ const dataBuffer = await readExactBytes(iterator, currentHeader.size, signal);
661
+ if (dataBuffer === void 0) {
662
+ throw new Error(`Unexpected end of stream while reading file data for ${currentHeader.path}`);
663
+ }
664
+ await skipPaddingBytesTo512Boundary(iterator, currentHeader.size, signal);
665
+ currentHeader.consumed = true;
666
+ return dataBuffer;
667
+ }
668
+ // For Readble stream
669
+ case "readable": {
670
+ const readable2 = createReadableFromIterator(iterator, currentHeader.size, signal, currentHeader);
671
+ return readable2;
672
+ }
673
+ default:
674
+ throw new Error(`Unsupported content type: ${type}`);
675
+ }
676
+ }
677
+ };
678
+ }
679
+ }
680
+ };
378
681
  export {
379
682
  createDirectoryItem,
683
+ createEntryItemGenerator,
380
684
  createFileItem,
381
685
  createGeneratorFileItem,
382
686
  createReadFileItem,
383
687
  createReadableFileItem,
688
+ createTarExtractor,
384
689
  createTarPacker,
385
- getBuffer,
690
+ extractTo,
386
691
  storeReaderToFile
387
692
  };
388
693
  //# sourceMappingURL=index.js.map