tar-vern 0.3.0 → 1.1.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.1.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: 6d4ff13b538b16545ccc55b2e74f8e5f73999a34
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, readdir } 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,85 @@ 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 });
194
208
  };
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
- });
209
+ const getAllFilesInDirectory = async (baseDir, signal) => {
210
+ const collectFiles = async (currentDir, relativePath) => {
211
+ signal?.throwIfAborted();
212
+ try {
213
+ const entries = await readdir(currentDir, { withFileTypes: true });
214
+ const result = [];
215
+ const tasks = entries.map(async (entry) => {
216
+ signal?.throwIfAborted();
217
+ const entryRelativePath = join(relativePath, entry.name);
218
+ if (entry.isDirectory()) {
219
+ const entryFullPath = join(currentDir, entry.name);
220
+ const directoryContents = await collectFiles(entryFullPath, entryRelativePath);
221
+ return [entryRelativePath, ...directoryContents];
222
+ } else {
223
+ return [entryRelativePath];
224
+ }
225
+ });
226
+ const allResults = await Promise.all(tasks);
227
+ for (const entryResults of allResults) {
228
+ result.push(...entryResults);
229
+ }
230
+ return result;
231
+ } catch (error) {
232
+ console.warn(`Warning: Could not read directory ${currentDir}:`, error);
233
+ return [];
234
+ }
235
+ };
236
+ return await collectFiles(baseDir, "");
237
+ };
238
+ const createEntryItemGenerator = async function* (baseDir, relativePaths, reflectStat, signal) {
239
+ const rs = reflectStat ?? "exceptName";
240
+ const pathsToProcess = relativePaths ?? await getAllFilesInDirectory(baseDir, signal);
241
+ for (const relativePath of pathsToProcess) {
242
+ signal?.throwIfAborted();
243
+ const fsPath = join(baseDir, relativePath);
244
+ try {
245
+ signal?.throwIfAborted();
246
+ const stats = await stat(fsPath);
247
+ if (stats.isDirectory()) {
248
+ yield await createDirectoryItem(relativePath, rs, {
249
+ directoryPath: fsPath
250
+ }, signal);
251
+ } else if (stats.isFile()) {
252
+ yield await createReadFileItem(relativePath, fsPath, rs, void 0, signal);
253
+ }
254
+ } catch (error) {
255
+ console.warn(`Warning: Could not access ${fsPath}:`, error);
256
+ continue;
257
+ }
258
+ }
259
+ };
260
+ const extractTo = async (iterator, basePath, signal) => {
261
+ for await (const entry of iterator) {
262
+ signal?.throwIfAborted();
263
+ const targetPath = join(basePath, entry.path);
264
+ if (entry.kind === "directory") {
265
+ try {
266
+ signal?.throwIfAborted();
267
+ await mkdir(targetPath, { recursive: true, mode: entry.mode });
268
+ } catch (error) {
269
+ if (error.code !== "EEXIST") {
270
+ throw error;
271
+ }
272
+ }
273
+ } else if (entry.kind === "file") {
274
+ const parentDir = dirname(targetPath);
275
+ signal?.throwIfAborted();
276
+ await mkdir(parentDir, { recursive: true });
277
+ const fileEntry = entry;
278
+ const content = await fileEntry.getContent("buffer");
279
+ await writeFile(targetPath, content, { mode: entry.mode, signal });
280
+ }
281
+ }
203
282
  };
204
283
  const utf8ByteLength = (str) => {
205
284
  return Buffer.byteLength(str, "utf8");
@@ -217,8 +296,6 @@ const truncateUtf8Safe = (str, maxBytes) => {
217
296
  }
218
297
  return str.slice(0, i);
219
298
  };
220
- const MAX_NAME = 100;
221
- const MAX_PREFIX = 155;
222
299
  const splitPath = (path) => {
223
300
  if (utf8ByteLength(path) <= MAX_NAME) {
224
301
  return { prefix: "", name: path };
@@ -299,10 +376,11 @@ const createTarPacker = (entryItemGenerator, compressionType, signal) => {
299
376
  const totalPaddedContentBytes = getPaddedBytes(contentBytes);
300
377
  yield totalPaddedContentBytes;
301
378
  } else {
379
+ const content = entryItemContent;
302
380
  const tarHeaderBytes = createTarHeader(
303
381
  "file",
304
382
  entryItem.path,
305
- entryItemContent.length,
383
+ content.length,
306
384
  entryItem.mode,
307
385
  entryItem.uname,
308
386
  entryItem.gname,
@@ -312,10 +390,10 @@ const createTarPacker = (entryItemGenerator, compressionType, signal) => {
312
390
  );
313
391
  yield tarHeaderBytes;
314
392
  let position = 0;
315
- switch (entryItemContent.kind) {
393
+ switch (content.kind) {
316
394
  // Content is a generator
317
395
  case "generator": {
318
- for await (const contentBytes of entryItemContent.generator) {
396
+ for await (const contentBytes of content.generator) {
319
397
  signal?.throwIfAborted();
320
398
  yield contentBytes;
321
399
  position += contentBytes.length;
@@ -324,9 +402,9 @@ const createTarPacker = (entryItemGenerator, compressionType, signal) => {
324
402
  }
325
403
  // Content is a readable stream
326
404
  case "readable": {
327
- for await (const content of entryItemContent.readable) {
405
+ for await (const chunk of content.readable) {
328
406
  signal?.throwIfAborted();
329
- const contentBytes = getBuffer(content);
407
+ const contentBytes = getBuffer(chunk);
330
408
  yield contentBytes;
331
409
  position += contentBytes.length;
332
410
  }
@@ -364,25 +442,282 @@ const createTarPacker = (entryItemGenerator, compressionType, signal) => {
364
442
  switch (ct) {
365
443
  // No compression
366
444
  case "none": {
367
- return Readable.from(entryItemIterator());
445
+ return Readable.from(entryItemIterator(), { signal });
368
446
  }
369
447
  // Gzip compression
370
448
  case "gzip": {
371
449
  const gzipStream = createGzip({ level: 9 });
372
- const entryItemStream = Readable.from(entryItemIterator());
450
+ const entryItemStream = Readable.from(entryItemIterator(), { signal });
373
451
  entryItemStream.pipe(gzipStream);
374
452
  return gzipStream;
375
453
  }
376
454
  }
377
455
  };
456
+ const parseOctalBytes = (buffer, offset, length) => {
457
+ const str = buffer.subarray(offset, offset + length).toString("ascii").replace(/\0/g, "").trim();
458
+ return str ? parseInt(str, 8) : 0;
459
+ };
460
+ const parseString = (buffer, offset, length) => {
461
+ return buffer.subarray(offset, offset + length).toString("utf8").replace(/\0/g, "").trim();
462
+ };
463
+ const readExactBytes = async (iterator, size, signal) => {
464
+ const chunks = [];
465
+ let totalRead = 0;
466
+ while (totalRead < size) {
467
+ signal?.throwIfAborted();
468
+ const { value, done } = await iterator.next();
469
+ if (done) {
470
+ if (totalRead === 0) {
471
+ return void 0;
472
+ } else {
473
+ throw new Error(`Unexpected end of stream: expected ${size} bytes, got ${totalRead} bytes`);
474
+ }
475
+ }
476
+ const chunk = getBuffer(value);
477
+ const needed = size - totalRead;
478
+ if (chunk.length <= needed) {
479
+ chunks.push(chunk);
480
+ totalRead += chunk.length;
481
+ } else {
482
+ chunks.push(chunk.subarray(0, needed));
483
+ await iterator.return?.(chunk.subarray(needed));
484
+ totalRead = size;
485
+ }
486
+ }
487
+ return Buffer.concat(chunks, size);
488
+ };
489
+ const skipExactBytes = async (iterator, size, signal) => {
490
+ let totalSkipped = 0;
491
+ while (totalSkipped < size) {
492
+ signal?.throwIfAborted();
493
+ const { value, done } = await iterator.next();
494
+ if (done) {
495
+ throw new Error(`Unexpected end of stream: expected to skip ${size} bytes, skipped ${totalSkipped} bytes`);
496
+ }
497
+ const chunk = getBuffer(value);
498
+ const needed = size - totalSkipped;
499
+ if (chunk.length <= needed) {
500
+ totalSkipped += chunk.length;
501
+ } else {
502
+ await iterator.return?.(chunk.subarray(needed));
503
+ totalSkipped = size;
504
+ }
505
+ }
506
+ };
507
+ const skipPaddingBytesTo512Boundary = async (iterator, contentSize, signal) => {
508
+ const padding = (512 - contentSize % 512) % 512;
509
+ if (padding > 0) {
510
+ await skipExactBytes(iterator, padding, signal);
511
+ }
512
+ };
513
+ const parseTarHeader = (buffer) => {
514
+ if (buffer.every((b) => b === 0)) {
515
+ return void 0;
516
+ }
517
+ const name = parseString(buffer, 0, 100);
518
+ const mode = parseOctalBytes(buffer, 100, 8);
519
+ const uid = parseOctalBytes(buffer, 108, 8);
520
+ const gid = parseOctalBytes(buffer, 116, 8);
521
+ const size = parseOctalBytes(buffer, 124, 12);
522
+ const mtime = new Date(parseOctalBytes(buffer, 136, 12) * 1e3);
523
+ const checksum = parseOctalBytes(buffer, 148, 8);
524
+ const typeflag = parseString(buffer, 156, 1);
525
+ const magic = parseString(buffer, 257, 6);
526
+ const uname = parseString(buffer, 265, 32);
527
+ const gname = parseString(buffer, 297, 32);
528
+ const prefix = parseString(buffer, 345, 155);
529
+ if (magic !== "ustar") {
530
+ throw new Error(`Invalid tar format: magic="${magic}"`);
531
+ }
532
+ let calculatedSum = 0;
533
+ for (let i = 0; i < 512; i++) {
534
+ if (i >= 148 && i < 156) {
535
+ calculatedSum += 32;
536
+ } else {
537
+ calculatedSum += buffer[i];
538
+ }
539
+ }
540
+ if (calculatedSum !== checksum) {
541
+ throw new Error(`Invalid checksum: expected ${checksum}, got ${calculatedSum}`);
542
+ }
543
+ let path = prefix ? `${prefix}/${name}` : name;
544
+ if (path.endsWith("/")) {
545
+ path = path.slice(0, -1);
546
+ }
547
+ const kind = typeflag === "5" ? "directory" : "file";
548
+ return {
549
+ kind,
550
+ path,
551
+ size,
552
+ mode,
553
+ uid,
554
+ gid,
555
+ mtime,
556
+ uname: uname || uid.toString(),
557
+ gname: gname || gid.toString(),
558
+ checksum,
559
+ consumed: false
560
+ };
561
+ };
562
+ const createBufferedAsyncIterator = (iterable, signal) => {
563
+ const buffer = [];
564
+ const iterator = iterable[Symbol.asyncIterator]();
565
+ return {
566
+ next: async () => {
567
+ signal?.throwIfAborted();
568
+ if (buffer.length > 0) {
569
+ return { value: buffer.shift(), done: false };
570
+ }
571
+ return iterator.next();
572
+ },
573
+ return: async (value) => {
574
+ if (value !== void 0) {
575
+ buffer.unshift(value);
576
+ }
577
+ return { value: void 0, done: false };
578
+ }
579
+ };
580
+ };
581
+ const createReadableFromIterator = (iterator, size, signal, consumedRef) => {
582
+ const generator = async function* () {
583
+ let remainingBytes = size;
584
+ while (remainingBytes > 0) {
585
+ signal?.throwIfAborted();
586
+ const { value, done } = await iterator.next();
587
+ if (done) {
588
+ throw new Error(`Unexpected end of stream: expected ${size} bytes, remaining ${remainingBytes} bytes`);
589
+ }
590
+ const chunk = getBuffer(value);
591
+ if (chunk.length <= remainingBytes) {
592
+ remainingBytes -= chunk.length;
593
+ yield chunk;
594
+ } else {
595
+ const needed = chunk.subarray(0, remainingBytes);
596
+ const excess = chunk.subarray(remainingBytes);
597
+ remainingBytes = 0;
598
+ await iterator.return?.(excess);
599
+ yield needed;
600
+ break;
601
+ }
602
+ }
603
+ await skipPaddingBytesTo512Boundary(iterator, size, signal);
604
+ consumedRef.consumed = true;
605
+ };
606
+ return Readable.from(generator(), { signal });
607
+ };
608
+ const createTarExtractor = async function* (readable, compressionType, signal) {
609
+ const ct = compressionType ?? "none";
610
+ let inputStream;
611
+ switch (ct) {
612
+ case "gzip":
613
+ const gunzip = createGunzip();
614
+ readable.pipe(gunzip);
615
+ inputStream = gunzip;
616
+ break;
617
+ case "none":
618
+ default:
619
+ inputStream = readable;
620
+ break;
621
+ }
622
+ const iterator = createBufferedAsyncIterator(inputStream, signal);
623
+ let header;
624
+ while (true) {
625
+ signal?.throwIfAborted();
626
+ if (header?.kind === "file" && !header.consumed) {
627
+ await skipExactBytes(iterator, header.size, signal);
628
+ await skipPaddingBytesTo512Boundary(iterator, header.size, signal);
629
+ header.consumed = true;
630
+ }
631
+ let headerBuffer;
632
+ try {
633
+ headerBuffer = await readExactBytes(iterator, 512, signal);
634
+ } catch (error) {
635
+ if (error instanceof Error && error.message.includes("Unexpected end of stream")) {
636
+ throw new Error("Invalid tar format: incomplete header");
637
+ }
638
+ throw error;
639
+ }
640
+ if (headerBuffer === void 0) {
641
+ break;
642
+ }
643
+ header = parseTarHeader(headerBuffer);
644
+ if (!header) {
645
+ const secondBlock = await readExactBytes(iterator, 512, signal);
646
+ if (secondBlock === void 0 || secondBlock.every((b) => b === 0)) {
647
+ break;
648
+ }
649
+ throw new Error("Invalid tar format: expected terminator block");
650
+ }
651
+ if (header.kind === "directory") {
652
+ yield {
653
+ kind: "directory",
654
+ path: header.path,
655
+ mode: header.mode,
656
+ uid: header.uid,
657
+ gid: header.gid,
658
+ uname: header.uname,
659
+ gname: header.gname,
660
+ date: header.mtime
661
+ };
662
+ } else {
663
+ const currentHeader = header;
664
+ yield {
665
+ kind: "file",
666
+ path: currentHeader.path,
667
+ mode: currentHeader.mode,
668
+ uid: currentHeader.uid,
669
+ gid: currentHeader.gid,
670
+ uname: currentHeader.uname,
671
+ gname: currentHeader.gname,
672
+ date: currentHeader.mtime,
673
+ getContent: async (type) => {
674
+ if (currentHeader.consumed) {
675
+ throw new Error("Content has already been consumed. Multiple calls to getContent are not supported.");
676
+ }
677
+ switch (type) {
678
+ // For string
679
+ case "string": {
680
+ const dataBuffer = await readExactBytes(iterator, currentHeader.size, signal);
681
+ if (dataBuffer === void 0) {
682
+ throw new Error(`Unexpected end of stream while reading file data for ${currentHeader.path}`);
683
+ }
684
+ await skipPaddingBytesTo512Boundary(iterator, currentHeader.size, signal);
685
+ currentHeader.consumed = true;
686
+ return dataBuffer.toString("utf8");
687
+ }
688
+ // For buffer
689
+ case "buffer": {
690
+ const dataBuffer = await readExactBytes(iterator, currentHeader.size, signal);
691
+ if (dataBuffer === void 0) {
692
+ throw new Error(`Unexpected end of stream while reading file data for ${currentHeader.path}`);
693
+ }
694
+ await skipPaddingBytesTo512Boundary(iterator, currentHeader.size, signal);
695
+ currentHeader.consumed = true;
696
+ return dataBuffer;
697
+ }
698
+ // For Readble stream
699
+ case "readable": {
700
+ const readable2 = createReadableFromIterator(iterator, currentHeader.size, signal, currentHeader);
701
+ return readable2;
702
+ }
703
+ default:
704
+ throw new Error(`Unsupported content type: ${type}`);
705
+ }
706
+ }
707
+ };
708
+ }
709
+ }
710
+ };
378
711
  export {
379
712
  createDirectoryItem,
713
+ createEntryItemGenerator,
380
714
  createFileItem,
381
715
  createGeneratorFileItem,
382
716
  createReadFileItem,
383
717
  createReadableFileItem,
718
+ createTarExtractor,
384
719
  createTarPacker,
385
- getBuffer,
720
+ extractTo,
386
721
  storeReaderToFile
387
722
  };
388
723
  //# sourceMappingURL=index.js.map