modern-tar 0.7.2 → 0.7.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/dist/fs/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as normalizeBody, c as LINK, l as SYMLINK, n as createTarPacker, o as DIRECTORY, r as transformHeader, s as FILE, t as createUnpacker } from "../unpacker-EHw0ivci.js";
1
+ import { a as normalizeBody, c as LINK, l as SYMLINK, n as createTarPacker, o as DIRECTORY, r as transformHeader, s as FILE, t as createUnpacker } from "../unpacker-CEuY-276.js";
2
2
  import * as fs from "node:fs/promises";
3
3
  import { cpus } from "node:os";
4
4
  import * as path from "node:path";
@@ -575,11 +575,13 @@ const createPathCache = (destDirPath, options) => {
575
575
  }
576
576
  const parentDir = path.dirname(outPath);
577
577
  switch (type) {
578
- case DIRECTORY:
578
+ case DIRECTORY: {
579
579
  pathConflicts.set(normalizedName, DIRECTORY);
580
- await prepareDirectory(outPath, dmode ?? mode);
580
+ const safeMode = mode ? mode & 511 : void 0;
581
+ await prepareDirectory(outPath, dmode ?? safeMode);
581
582
  if (mtime) await fs.lutimes(outPath, mtime, mtime).catch(() => {});
582
583
  return;
584
+ }
583
585
  case FILE:
584
586
  pathConflicts.set(normalizedName, FILE);
585
587
  await prepareDirectory(parentDir);
@@ -633,37 +635,34 @@ function unpackTar(directoryPath, options = {}) {
633
635
  const pathCache = createPathCache(directoryPath, options);
634
636
  let currentFileStream = null;
635
637
  let currentWriteCallback = null;
636
- let needsDiscardBody = false;
637
638
  return new Writable({
638
639
  async write(chunk, _, cb) {
639
640
  try {
640
641
  unpacker.write(chunk);
641
- if (needsDiscardBody) {
642
- if (!unpacker.skipEntry()) {
643
- cb();
644
- return;
645
- }
646
- needsDiscardBody = false;
647
- }
648
- if (currentFileStream && currentWriteCallback) {
649
- let needsDrain = false;
650
- const writeCallback = currentWriteCallback;
651
- while (!unpacker.isBodyComplete()) {
652
- needsDrain = false;
653
- if (unpacker.streamBody(writeCallback) === 0) if (needsDrain) await currentFileStream.waitDrain();
654
- else {
642
+ if (unpacker.isEntryActive()) {
643
+ if (currentFileStream && currentWriteCallback) {
644
+ let needsDrain = false;
645
+ const writeCallback = currentWriteCallback;
646
+ while (!unpacker.isBodyComplete()) {
647
+ needsDrain = false;
648
+ if (unpacker.streamBody(writeCallback) === 0) if (needsDrain) await currentFileStream.waitDrain();
649
+ else {
650
+ cb();
651
+ return;
652
+ }
653
+ }
654
+ while (!unpacker.skipPadding()) {
655
655
  cb();
656
656
  return;
657
657
  }
658
- }
659
- while (!unpacker.skipPadding()) {
658
+ const streamToClose = currentFileStream;
659
+ if (streamToClose) opQueue.add(() => streamToClose.end());
660
+ currentFileStream = null;
661
+ currentWriteCallback = null;
662
+ } else if (!unpacker.skipEntry()) {
660
663
  cb();
661
664
  return;
662
665
  }
663
- const streamToClose = currentFileStream;
664
- if (streamToClose) opQueue.add(() => streamToClose.end());
665
- currentFileStream = null;
666
- currentWriteCallback = null;
667
666
  }
668
667
  while (true) {
669
668
  const header = unpacker.readHeader();
@@ -674,7 +673,6 @@ function unpackTar(directoryPath, options = {}) {
674
673
  const transformedHeader = transformHeader(header, options);
675
674
  if (!transformedHeader) {
676
675
  if (!unpacker.skipEntry()) {
677
- needsDiscardBody = true;
678
676
  cb();
679
677
  return;
680
678
  }
@@ -682,8 +680,9 @@ function unpackTar(directoryPath, options = {}) {
682
680
  }
683
681
  const outPath = await opQueue.add(() => pathCache.preparePath(transformedHeader));
684
682
  if (outPath) {
683
+ const safeMode = transformedHeader.mode ? transformedHeader.mode & 511 : void 0;
685
684
  const fileStream = createFileSink(outPath, {
686
- mode: options.fmode ?? transformedHeader.mode ?? void 0,
685
+ mode: options.fmode ?? safeMode,
687
686
  mtime: transformedHeader.mtime ?? void 0
688
687
  });
689
688
  let needsDrain = false;
@@ -710,7 +709,6 @@ function unpackTar(directoryPath, options = {}) {
710
709
  }
711
710
  opQueue.add(() => fileStream.end());
712
711
  } else if (!unpacker.skipEntry()) {
713
- needsDiscardBody = true;
714
712
  cb();
715
713
  return;
716
714
  }
@@ -108,7 +108,7 @@ function readNumeric(view, offset, size) {
108
108
 
109
109
  //#endregion
110
110
  //#region src/tar/body.ts
111
- const isBodyless = (header) => header.type === DIRECTORY || header.type === SYMLINK || header.type === LINK;
111
+ const isBodyless = (header) => header.type === DIRECTORY || header.type === SYMLINK || header.type === LINK || header.type === "character-device" || header.type === "block-device" || header.type === "fifo";
112
112
  async function normalizeBody(body) {
113
113
  if (body === null || body === void 0) return EMPTY;
114
114
  if (body instanceof Uint8Array) return body;
@@ -256,6 +256,7 @@ function parseUstarHeader(block, strict) {
256
256
  linkname: readString(block, USTAR_LINKNAME_OFFSET, USTAR_LINKNAME_SIZE)
257
257
  };
258
258
  const magic = readString(block, USTAR_MAGIC_OFFSET, USTAR_MAGIC_SIZE);
259
+ if (isBodyless(header)) header.size = 0;
259
260
  if (magic.trim() === "ustar") {
260
261
  header.uname = readString(block, USTAR_UNAME_OFFSET, USTAR_UNAME_SIZE);
261
262
  header.gname = readString(block, USTAR_GNAME_OFFSET, USTAR_GNAME_SIZE);
@@ -275,8 +276,8 @@ const PAX_MAPPING = {
275
276
  };
276
277
  function parsePax(buffer) {
277
278
  const decoder$1 = new TextDecoder("utf-8");
278
- const overrides = {};
279
- const pax = {};
279
+ const overrides = Object.create(null);
280
+ const pax = Object.create(null);
280
281
  let offset = 0;
281
282
  while (offset < buffer.length) {
282
283
  const spaceIndex = buffer.indexOf(32, offset);
@@ -287,9 +288,8 @@ function parsePax(buffer) {
287
288
  const [key, value] = decoder$1.decode(buffer.subarray(spaceIndex + 1, recordEnd - 1)).split("=", 2);
288
289
  if (key && value !== void 0) {
289
290
  pax[key] = value;
290
- const mapping = PAX_MAPPING[key];
291
- if (mapping) {
292
- const [targetKey, parser] = mapping;
291
+ if (Object.hasOwn(PAX_MAPPING, key)) {
292
+ const [targetKey, parser] = PAX_MAPPING[key];
293
293
  const parsedValue = parser(value);
294
294
  if (typeof parsedValue === "string" || !Number.isNaN(parsedValue)) overrides[targetKey] = parsedValue;
295
295
  }
@@ -562,16 +562,20 @@ function createChunkQueue() {
562
562
  //#region src/tar/unpacker.ts
563
563
  const STATE_HEADER = 0;
564
564
  const STATE_BODY = 1;
565
+ const truncateErr = /* @__PURE__ */ new Error("Tar archive is truncated.");
565
566
  function createUnpacker(options = {}) {
566
567
  const strict = options.strict ?? false;
567
568
  const { available, peek, push, discard, pull } = createChunkQueue();
568
569
  let state = STATE_HEADER;
569
570
  let ended = false;
571
+ let done = false;
570
572
  let eof = false;
571
573
  let currentEntry = null;
572
574
  const paxGlobals = {};
573
575
  let nextEntryOverrides = {};
574
576
  const unpacker = {
577
+ isEntryActive: () => state === STATE_BODY,
578
+ isBodyComplete: () => !currentEntry || currentEntry.remaining === 0,
575
579
  write(chunk) {
576
580
  if (ended) throw new Error("Archive already ended.");
577
581
  push(chunk);
@@ -581,12 +585,12 @@ function createUnpacker(options = {}) {
581
585
  },
582
586
  readHeader() {
583
587
  if (state !== STATE_HEADER) throw new Error("Cannot read header while an entry is active");
584
- if (eof) return void 0;
585
- while (!eof) {
588
+ if (done) return void 0;
589
+ while (!done) {
586
590
  if (available() < BLOCK_SIZE) {
587
591
  if (ended) {
588
- if (available() > 0 && strict) throw new Error("Tar archive is truncated.");
589
- eof = true;
592
+ if (available() > 0 && strict) throw truncateErr;
593
+ done = true;
590
594
  return;
591
595
  }
592
596
  return null;
@@ -595,14 +599,15 @@ function createUnpacker(options = {}) {
595
599
  if (isZeroBlock(headerBlock)) {
596
600
  if (available() < BLOCK_SIZE * 2) {
597
601
  if (ended) {
598
- if (strict) throw new Error("Tar archive is truncated.");
599
- eof = true;
602
+ if (strict) throw truncateErr;
603
+ done = true;
600
604
  return;
601
605
  }
602
606
  return null;
603
607
  }
604
608
  if (isZeroBlock(peek(BLOCK_SIZE * 2).subarray(BLOCK_SIZE))) {
605
609
  discard(BLOCK_SIZE * 2);
610
+ done = true;
606
611
  eof = true;
607
612
  return;
608
613
  }
@@ -622,7 +627,7 @@ function createUnpacker(options = {}) {
622
627
  if (metaParser) {
623
628
  const paddedSize = internalHeader.size + BLOCK_SIZE_MASK & ~BLOCK_SIZE_MASK;
624
629
  if (available() < BLOCK_SIZE + paddedSize) {
625
- if (ended && strict) throw new Error("Tar archive is truncated.");
630
+ if (ended && strict) throw truncateErr;
626
631
  return null;
627
632
  }
628
633
  discard(BLOCK_SIZE);
@@ -636,6 +641,7 @@ function createUnpacker(options = {}) {
636
641
  if (internalHeader.prefix) header.name = `${internalHeader.prefix}/${header.name}`;
637
642
  applyOverrides(header, paxGlobals);
638
643
  applyOverrides(header, nextEntryOverrides);
644
+ if (header.name.endsWith("/") && header.type === FILE) header.type = DIRECTORY;
639
645
  nextEntryOverrides = {};
640
646
  currentEntry = {
641
647
  header,
@@ -654,9 +660,6 @@ function createUnpacker(options = {}) {
654
660
  currentEntry.remaining -= fed;
655
661
  return fed;
656
662
  },
657
- isBodyComplete() {
658
- return !currentEntry || currentEntry.remaining === 0;
659
- },
660
663
  skipPadding() {
661
664
  if (state !== STATE_BODY || !currentEntry) return true;
662
665
  if (currentEntry.remaining > 0) throw new Error("Body not fully consumed");
@@ -668,12 +671,20 @@ function createUnpacker(options = {}) {
668
671
  },
669
672
  skipEntry() {
670
673
  if (state !== STATE_BODY || !currentEntry) return true;
671
- while (!unpacker.isBodyComplete()) if (unpacker.streamBody(() => true) === 0) return false;
674
+ const toDiscard = Math.min(currentEntry.remaining, available());
675
+ if (toDiscard > 0) {
676
+ discard(toDiscard);
677
+ currentEntry.remaining -= toDiscard;
678
+ }
679
+ if (currentEntry.remaining > 0) return false;
672
680
  return unpacker.skipPadding();
673
681
  },
674
682
  validateEOF() {
675
- if (strict && available() > 0) {
676
- if (pull(available()).some((byte) => byte !== 0)) throw new Error("Invalid EOF.");
683
+ if (strict) {
684
+ if (!eof) throw truncateErr;
685
+ if (available() > 0) {
686
+ if (pull(available()).some((byte) => byte !== 0)) throw new Error("Invalid EOF.");
687
+ }
677
688
  }
678
689
  }
679
690
  };
package/dist/web/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as normalizeBody, i as isBodyless, n as createTarPacker$1, r as transformHeader, t as createUnpacker } from "../unpacker-EHw0ivci.js";
1
+ import { a as normalizeBody, i as isBodyless, n as createTarPacker$1, r as transformHeader, t as createUnpacker } from "../unpacker-CEuY-276.js";
2
2
 
3
3
  //#region src/web/compression.ts
4
4
  function createGzipEncoder() {
@@ -71,6 +71,7 @@ async function streamToBuffer(stream) {
71
71
  reader.releaseLock();
72
72
  }
73
73
  }
74
+ const drain = (stream) => stream.pipeTo(new WritableStream());
74
75
 
75
76
  //#endregion
76
77
  //#region src/web/unpack.ts
@@ -78,93 +79,45 @@ function createTarDecoder(options = {}) {
78
79
  const unpacker = createUnpacker(options);
79
80
  let bodyController = null;
80
81
  let pumping = false;
81
- let controllerTerminated = false;
82
- let eofReached = false;
83
- const abortAll = (reason, controller) => {
84
- if (controllerTerminated) return;
85
- controllerTerminated = true;
86
- const error = reason instanceof Error ? reason : new Error(String(reason ?? ""));
87
- if (bodyController) {
88
- try {
89
- bodyController.error(error);
90
- } catch {}
91
- bodyController = null;
92
- }
93
- if (controller) try {
94
- controller.error(error);
95
- } catch {}
96
- };
97
- const pump = (controller, force = false) => {
98
- if (pumping || controllerTerminated || eofReached) return;
82
+ const pump = (controller) => {
83
+ if (pumping) return;
99
84
  pumping = true;
100
85
  try {
101
- while (!controllerTerminated) {
102
- if (!bodyController) {
103
- if (!unpacker.skipEntry()) break;
104
- if (!force && (controller.desiredSize ?? 0) < 0) break;
105
- const header = unpacker.readHeader();
106
- if (header === null) break;
107
- if (header === void 0) {
108
- eofReached = true;
109
- break;
110
- }
111
- const body = new ReadableStream({
112
- start: (c) => bodyController = c,
86
+ while (true) if (unpacker.isEntryActive()) {
87
+ if (bodyController) {
88
+ if (unpacker.streamBody((c) => (bodyController.enqueue(c), true)) === 0 && !unpacker.isBodyComplete()) break;
89
+ } else if (!unpacker.skipEntry()) break;
90
+ if (unpacker.isBodyComplete()) {
91
+ try {
92
+ bodyController?.close();
93
+ } catch {}
94
+ bodyController = null;
95
+ if (!unpacker.skipPadding()) break;
96
+ }
97
+ } else {
98
+ const header = unpacker.readHeader();
99
+ if (header === null || header === void 0) break;
100
+ controller.enqueue({
101
+ header,
102
+ body: new ReadableStream({
103
+ start(c) {
104
+ if (header.size === 0) c.close();
105
+ else bodyController = c;
106
+ },
113
107
  pull: () => pump(controller),
114
- cancel: () => {
108
+ cancel() {
115
109
  bodyController = null;
116
110
  pump(controller);
117
111
  }
118
- });
119
- controller.enqueue({
120
- header,
121
- body
122
- });
123
- if (header.size === 0) {
124
- try {
125
- bodyController.close();
126
- } catch {}
127
- bodyController = null;
128
- if (!unpacker.skipPadding()) break;
129
- continue;
130
- }
131
- }
132
- if (unpacker.isBodyComplete()) {
133
- if (unpacker.skipPadding()) {
134
- try {
135
- bodyController.close();
136
- } catch {}
137
- bodyController = null;
138
- continue;
139
- }
140
- break;
141
- }
142
- if ((bodyController.desiredSize ?? 1) <= 0) break;
143
- let shouldPause = false;
144
- if (unpacker.streamBody((chunk) => {
145
- if (!bodyController) return true;
146
- try {
147
- bodyController.enqueue(chunk);
148
- if ((bodyController.desiredSize ?? 1) <= 0) shouldPause = true;
149
- } catch {
150
- return true;
151
- }
152
- return true;
153
- }) === 0) break;
154
- if (unpacker.isBodyComplete()) {
155
- if (unpacker.skipPadding()) {
156
- try {
157
- bodyController.close();
158
- } catch {}
159
- bodyController = null;
160
- continue;
161
- }
162
- break;
163
- }
164
- if (shouldPause) break;
112
+ })
113
+ });
165
114
  }
166
115
  } catch (error) {
167
- abortAll(error, controller);
116
+ try {
117
+ bodyController?.error(error);
118
+ } catch {}
119
+ bodyController = null;
120
+ throw error;
168
121
  } finally {
169
122
  pumping = false;
170
123
  }
@@ -172,29 +125,27 @@ function createTarDecoder(options = {}) {
172
125
  return new TransformStream({
173
126
  transform(chunk, controller) {
174
127
  try {
175
- if (eofReached && options.strict && chunk.some((byte) => byte !== 0)) throw new Error("Invalid EOF.");
176
128
  unpacker.write(chunk);
177
129
  pump(controller);
178
130
  } catch (error) {
179
- abortAll(error, controller);
131
+ try {
132
+ bodyController?.error(error);
133
+ } catch {}
180
134
  throw error;
181
135
  }
182
136
  },
183
137
  flush(controller) {
184
138
  try {
185
139
  unpacker.end();
186
- pump(controller, true);
187
- if (bodyController) {
188
- if (options.strict) throw new Error("Tar archive is truncated.");
189
- try {
190
- bodyController.close();
191
- } catch {}
192
- bodyController = null;
193
- }
140
+ pump(controller);
194
141
  unpacker.validateEOF();
195
- if (!controllerTerminated) controller.terminate();
142
+ if (unpacker.isEntryActive() && !unpacker.isBodyComplete()) try {
143
+ bodyController?.close();
144
+ } catch {}
196
145
  } catch (error) {
197
- abortAll(error, controller);
146
+ try {
147
+ bodyController?.error(error);
148
+ } catch {}
198
149
  throw error;
199
150
  }
200
151
  }
@@ -245,15 +196,12 @@ async function unpackTar(archive, options = {}) {
245
196
  throw error;
246
197
  }
247
198
  if (processedHeader === null) {
248
- await entry.body.cancel();
199
+ await drain(entry.body);
249
200
  continue;
250
201
  }
251
202
  if (isBodyless(processedHeader)) {
252
- await entry.body.cancel();
253
- results.push({
254
- header: processedHeader,
255
- data: void 0
256
- });
203
+ await drain(entry.body);
204
+ results.push({ header: processedHeader });
257
205
  } else results.push({
258
206
  header: processedHeader,
259
207
  data: await streamToBuffer(entry.body)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modern-tar",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Zero dependency streaming tar parser and writer for JavaScript.",
5
5
  "author": "Ayuhito <hello@ayuhito.com>",
6
6
  "license": "MIT",
@@ -25,12 +25,13 @@
25
25
  }
26
26
  },
27
27
  "devDependencies": {
28
- "@biomejs/biome": "2.3.5",
29
- "@types/node": "^24.10.1",
30
- "@vitest/coverage-v8": "^4.0.8",
31
- "tsdown": "^0.16.3",
28
+ "@biomejs/biome": "2.3.8",
29
+ "@types/node": "^25.0.2",
30
+ "@vitest/browser-playwright": "4.0.15",
31
+ "@vitest/coverage-v8": "4.0.15",
32
+ "tsdown": "^0.18.0",
32
33
  "typescript": "^5.9.3",
33
- "vitest": "^4.0.8"
34
+ "vitest": "4.0.15"
34
35
  },
35
36
  "scripts": {
36
37
  "build": "tsdown",
@@ -38,7 +39,8 @@
38
39
  "test": "vitest",
39
40
  "coverage": "vitest run --coverage",
40
41
  "check": "biome check --write",
41
- "typecheck": "tsc --noEmit"
42
+ "typecheck": "tsc --noEmit",
43
+ "test:browser": "vitest --config=vitest.browser.config.ts --browser"
42
44
  },
43
45
  "files": [
44
46
  "dist",