modern-tar 0.0.0 → 0.2.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/README.md CHANGED
@@ -173,6 +173,25 @@ const extractStream = unpackTar('./output', {
173
173
  await pipeline(sourceStream, extractStream);
174
174
  ```
175
175
 
176
+ #### Archive Creation
177
+
178
+ ```typescript
179
+ import { packTarSources, type TarSource } from 'modern-tar/fs';
180
+ import { createWriteStream } from 'node:fs';
181
+ import { pipeline } from 'node:stream/promises';
182
+
183
+ // Pack multiple sources
184
+ const sources: TarSource[] = [
185
+ { type: 'file', source: './package.json', target: 'project/package.json' },
186
+ { type: 'directory', source: './src', target: 'project/src' },
187
+ { type: 'content', content: 'Hello World!', target: 'project/hello.txt' },
188
+ { type: 'content', content: '#!/bin/bash\necho "Executable"', target: 'bin/script.sh', mode: 0o755 }
189
+ ];
190
+
191
+ const archiveStream = packTarSources(sources);
192
+ await pipeline(archiveStream, createWriteStream('project.tar'));
193
+ ```
194
+
176
195
  ## API Reference
177
196
 
178
197
  ### Core API (`modern-tar`)
@@ -339,6 +358,31 @@ const extractStream = unpackTar('/restore/location', {
339
358
  await pipeline(tarStream, extractStream);
340
359
  ```
341
360
 
361
+ #### `packTarSources(sources: TarSource[]): Readable`
362
+
363
+ Pack multiple sources (files, directories, or raw content) into a tar archive stream.
364
+
365
+ - **`sources`**: Array of `TarSource` objects describing what to include in the archive.
366
+ - **Returns**: Node.js `Readable` stream of tar archive bytes.
367
+
368
+ **Example:**
369
+
370
+ ```typescript
371
+ import { packTarSources, type TarSource } from 'modern-tar/fs';
372
+ import { createWriteStream } from 'node:fs';
373
+ import { pipeline } from 'node:stream/promises';
374
+
375
+ const sources: TarSource[] = [
376
+ { type: 'file', source: './README.md', target: 'docs/readme.txt' },
377
+ { type: 'directory', source: './src', target: 'app/src' },
378
+ { type: 'content', content: '{"version": "1.0.0"}', target: 'app/config.json' },
379
+ { type: 'content', content: Buffer.from('binary data'), target: 'data/binary.dat' }
380
+ ];
381
+
382
+ const archiveStream = packTarSources(sources);
383
+ await pipeline(archiveStream, createWriteStream('app.tar'));
384
+ ```
385
+
342
386
  ## Types
343
387
 
344
388
  ### Core Types
@@ -400,6 +444,35 @@ interface PackOptionsFS {
400
444
  map?: (header: TarHeader) => TarHeader;
401
445
  }
402
446
 
447
+ // Source types for packTarSources function
448
+ interface FileSource {
449
+ type: "file";
450
+ /** Path to the source file on the local filesystem */
451
+ source: string;
452
+ /** Destination path inside the tar archive */
453
+ target: string;
454
+ }
455
+
456
+ interface DirectorySource {
457
+ type: "directory";
458
+ /** Path to the source directory on the local filesystem */
459
+ source: string;
460
+ /** Destination path inside the tar archive */
461
+ target: string;
462
+ }
463
+
464
+ interface ContentSource {
465
+ type: "content";
466
+ /** Raw content to add. Supports string, Uint8Array, ArrayBuffer, ReadableStream, Blob, or null. */
467
+ content: TarEntryData;
468
+ /** Destination path inside the tar archive */
469
+ target: string;
470
+ /** Optional Unix file permissions (e.g., 0o644, 0o755) */
471
+ mode?: number;
472
+ }
473
+
474
+ type TarSource = FileSource | DirectorySource | ContentSource;
475
+
403
476
  interface UnpackOptionsFS extends UnpackOptions {
404
477
  // Inherited from UnpackOptions (platform-neutral):
405
478
  /** Number of leading path components to strip from entry names */
@@ -1,8 +1,8 @@
1
- import { TarHeader, UnpackOptions } from "../index-BLp7i3zL.js";
1
+ import { TarEntryData, TarHeader, UnpackOptions } from "../index-Dx4tbuJh.js";
2
2
  import { Stats } from "node:fs";
3
3
  import { Readable, Writable } from "node:stream";
4
4
 
5
- //#region src/fs/pack.d.ts
5
+ //#region src/fs/types.d.ts
6
6
 
7
7
  /**
8
8
  * Filesystem-specific configuration options for packing directories into tar archives.
@@ -18,6 +18,78 @@ interface PackOptionsFS {
18
18
  /** Transform function to modify tar headers before packing */
19
19
  map?: (header: TarHeader) => TarHeader;
20
20
  }
21
+ /**
22
+ * Filesystem-specific configuration options for extracting tar archives to the filesystem.
23
+ *
24
+ * Extends the core {@link UnpackOptions} with Node.js filesystem-specific settings
25
+ * for controlling file permissions and other filesystem behaviors.
26
+ */
27
+ interface UnpackOptionsFS extends UnpackOptions {
28
+ /** Default mode for created directories (e.g., 0o755). If not specified, uses mode from tar header or system default */
29
+ dmode?: number;
30
+ /** Default mode for created files (e.g., 0o644). If not specified, uses mode from tar header or system default */
31
+ fmode?: number;
32
+ /**
33
+ * Prevent symlinks from pointing outside the extraction directory.
34
+ * @default true
35
+ */
36
+ validateSymlinks?: boolean;
37
+ }
38
+ /** Describes a file on the local filesystem to be added to the archive. */
39
+ interface FileSource {
40
+ type: "file";
41
+ /** Path to the source file on the local filesystem. */
42
+ source: string;
43
+ /** Destination path for the file inside the tar archive. */
44
+ target: string;
45
+ }
46
+ /** Describes a directory on the local filesystem to be added to the archive. */
47
+ interface DirectorySource {
48
+ type: "directory";
49
+ /** Path to the source directory on the local filesystem. */
50
+ source: string;
51
+ /** Destination path for the directory inside the tar archive. */
52
+ target: string;
53
+ }
54
+ /** Describes raw content to be added to the archive. Supports all TarEntryData types including strings, buffers, streams, blobs, and null. */
55
+ interface ContentSource {
56
+ type: "content";
57
+ /** Raw content to add. Supports string, Uint8Array, ArrayBuffer, ReadableStream, Blob, or null. */
58
+ content: TarEntryData;
59
+ /** Destination path for the content inside the tar archive. */
60
+ target: string;
61
+ /** Optional Unix file permissions for the entry (e.g., 0o644). */
62
+ mode?: number;
63
+ }
64
+ /** A union of all possible source types for creating a tar archive. */
65
+ type TarSource = FileSource | DirectorySource | ContentSource;
66
+ //#endregion
67
+ //#region src/fs/archive.d.ts
68
+ /**
69
+ * Packs multiple sources into a tar archive as a Node.js Readable stream from an
70
+ * array of sources (files, directories, or raw content).
71
+ *
72
+ * @param sources - An array of {@link TarSource} objects describing what to include.
73
+ * @returns A Node.js [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable)
74
+ * stream that outputs the tar archive bytes.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * import { packTarSources, TarSource } from 'modern-tar/fs';
79
+ *
80
+ * const sources: TarSource[] = [
81
+ * { type: 'file', source: './package.json', target: 'project/package.json' },
82
+ * { type: 'directory', source: './src', target: 'project/src' },
83
+ * { type: 'content', content: 'hello world', target: 'project/hello.txt' }
84
+ * ];
85
+ *
86
+ * const archiveStream = packTarSources(sources);
87
+ * await pipeline(archiveStream, createWriteStream('project.tar'));
88
+ * ```
89
+ */
90
+ declare function packTarSources(sources: TarSource[]): Readable;
91
+ //#endregion
92
+ //#region src/fs/pack.d.ts
21
93
  /**
22
94
  * Pack a directory into a Node.js [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable) stream containing tar archive bytes.
23
95
  *
@@ -49,23 +121,6 @@ interface PackOptionsFS {
49
121
  declare function packTar(directoryPath: string, options?: PackOptionsFS): Readable;
50
122
  //#endregion
51
123
  //#region src/fs/unpack.d.ts
52
- /**
53
- * Filesystem-specific configuration options for extracting tar archives to the filesystem.
54
- *
55
- * Extends the core {@link UnpackOptions} with Node.js filesystem-specific settings
56
- * for controlling file permissions and other filesystem behaviors.
57
- */
58
- interface UnpackOptionsFS extends UnpackOptions {
59
- /** Default mode for created directories (e.g., 0o755). If not specified, uses mode from tar header or system default */
60
- dmode?: number;
61
- /** Default mode for created files (e.g., 0o644). If not specified, uses mode from tar header or system default */
62
- fmode?: number;
63
- /**
64
- * Prevent symlinks from pointing outside the extraction directory.
65
- * @default true
66
- */
67
- validateSymlinks?: boolean;
68
- }
69
124
  /**
70
125
  * Extract a tar archive to a directory.
71
126
  *
@@ -99,4 +154,4 @@ interface UnpackOptionsFS extends UnpackOptions {
99
154
  */
100
155
  declare function unpackTar(directoryPath: string, options?: UnpackOptionsFS): Writable;
101
156
  //#endregion
102
- export { type PackOptionsFS, type UnpackOptionsFS, packTar, unpackTar };
157
+ export { type ContentSource, type DirectorySource, type FileSource, type PackOptionsFS, type TarSource, type UnpackOptionsFS, packTar, packTarSources, unpackTar };
package/dist/fs/index.js CHANGED
@@ -1,10 +1,121 @@
1
- import { BLOCK_SIZE, createTarDecoder, createTarHeader, createTarOptionsTransformer } from "../web-BsjPG7md.js";
1
+ import { BLOCK_SIZE, createTarDecoder, createTarHeader, createTarOptionsTransformer, createTarPacker } from "../web-C1b5fAZt.js";
2
2
  import { createReadStream, createWriteStream } from "node:fs";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path from "node:path";
5
5
  import { PassThrough, Readable, Writable } from "node:stream";
6
6
  import { pipeline } from "node:stream/promises";
7
7
 
8
+ //#region src/fs/archive.ts
9
+ async function addFileToPacker(controller, sourcePath, targetPath) {
10
+ const stat = await fs.stat(sourcePath);
11
+ const entryStream = controller.add({
12
+ name: targetPath,
13
+ size: stat.size,
14
+ mode: stat.mode,
15
+ mtime: stat.mtime,
16
+ type: "file"
17
+ });
18
+ await pipeline(createReadStream(sourcePath), Writable.fromWeb(entryStream));
19
+ }
20
+ async function addDirectoryToPacker(controller, sourcePath, targetPathInArchive) {
21
+ const sourceStat = await fs.stat(sourcePath);
22
+ controller.add({
23
+ name: `${targetPathInArchive}/`,
24
+ type: "directory",
25
+ mode: sourceStat.mode,
26
+ mtime: sourceStat.mtime,
27
+ size: 0
28
+ }).close();
29
+ const dirents = await fs.readdir(sourcePath, { withFileTypes: true });
30
+ for (const dirent of dirents) {
31
+ const fullSourcePath = path.join(sourcePath, dirent.name);
32
+ const archiveEntryPath = path.join(targetPathInArchive, dirent.name).replace(/\\/g, "/");
33
+ if (dirent.isDirectory()) await addDirectoryToPacker(controller, fullSourcePath, archiveEntryPath);
34
+ else if (dirent.isFile()) await addFileToPacker(controller, fullSourcePath, archiveEntryPath);
35
+ }
36
+ }
37
+ /**
38
+ * Packs multiple sources into a tar archive as a Node.js Readable stream from an
39
+ * array of sources (files, directories, or raw content).
40
+ *
41
+ * @param sources - An array of {@link TarSource} objects describing what to include.
42
+ * @returns A Node.js [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable)
43
+ * stream that outputs the tar archive bytes.
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * import { packTarSources, TarSource } from 'modern-tar/fs';
48
+ *
49
+ * const sources: TarSource[] = [
50
+ * { type: 'file', source: './package.json', target: 'project/package.json' },
51
+ * { type: 'directory', source: './src', target: 'project/src' },
52
+ * { type: 'content', content: 'hello world', target: 'project/hello.txt' }
53
+ * ];
54
+ *
55
+ * const archiveStream = packTarSources(sources);
56
+ * await pipeline(archiveStream, createWriteStream('project.tar'));
57
+ * ```
58
+ */
59
+ function packTarSources(sources) {
60
+ const { readable, controller } = createTarPacker();
61
+ (async () => {
62
+ for (const source of sources) {
63
+ const targetPath = source.target.replace(/\\/g, "/");
64
+ switch (source.type) {
65
+ case "file":
66
+ await addFileToPacker(controller, source.source, targetPath);
67
+ break;
68
+ case "directory":
69
+ await addDirectoryToPacker(controller, source.source, targetPath);
70
+ break;
71
+ case "content": {
72
+ const { content, mode } = source;
73
+ if (content instanceof Blob) {
74
+ const entryStream = controller.add({
75
+ name: targetPath,
76
+ size: content.size,
77
+ mode,
78
+ type: "file"
79
+ });
80
+ await content.stream().pipeTo(entryStream);
81
+ break;
82
+ }
83
+ if (content instanceof ReadableStream) {
84
+ const chunks = [];
85
+ for await (const chunk of Readable.fromWeb(content)) chunks.push(chunk);
86
+ const buffer = Buffer.concat(chunks);
87
+ const writer$1 = controller.add({
88
+ name: targetPath,
89
+ size: buffer.length,
90
+ mode,
91
+ type: "file"
92
+ }).getWriter();
93
+ await writer$1.write(buffer);
94
+ await writer$1.close();
95
+ break;
96
+ }
97
+ let data;
98
+ if (content === null || content === void 0) data = new Uint8Array(0);
99
+ else if (typeof content === "string") data = Buffer.from(content);
100
+ else if (content instanceof ArrayBuffer) data = new Uint8Array(content);
101
+ else data = content;
102
+ const writer = controller.add({
103
+ name: targetPath,
104
+ size: data.length,
105
+ mode,
106
+ type: "file"
107
+ }).getWriter();
108
+ await writer.write(data);
109
+ await writer.close();
110
+ break;
111
+ }
112
+ }
113
+ }
114
+ })().then(() => controller.finalize()).catch((err) => controller.error(err));
115
+ return Readable.fromWeb(readable);
116
+ }
117
+
118
+ //#endregion
8
119
  //#region src/fs/pack.ts
9
120
  /**
10
121
  * Pack a directory into a Node.js [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable) stream containing tar archive bytes.
@@ -119,58 +230,17 @@ function packTar(directoryPath, options = {}) {
119
230
  */
120
231
  function unpackTar(directoryPath, options = {}) {
121
232
  const bridge = new PassThrough();
122
- const webReadable = Readable.toWeb(bridge);
123
- const processingPromise = (async () => {
124
- const { validateSymlinks = true } = options;
125
- const resolvedDestDir = path.resolve(directoryPath);
126
- const createdDirs = /* @__PURE__ */ new Set();
127
- await fs.mkdir(resolvedDestDir, { recursive: true });
128
- createdDirs.add(resolvedDestDir);
129
- const entryStream = webReadable.pipeThrough(createTarDecoder()).pipeThrough(createTarOptionsTransformer(options));
130
- for await (const entry of entryStream) {
131
- const header = entry.header;
132
- const outPath = path.join(directoryPath, header.name);
133
- const parentDir = path.dirname(outPath);
134
- if (!createdDirs.has(parentDir)) {
135
- await fs.mkdir(parentDir, { recursive: true });
136
- createdDirs.add(parentDir);
137
- }
138
- switch (header.type) {
139
- case "directory": {
140
- const mode = options.dmode ?? header.mode;
141
- if (createdDirs.has(outPath) && mode) await fs.chmod(outPath, mode);
142
- else {
143
- await fs.mkdir(outPath, {
144
- recursive: true,
145
- mode
146
- });
147
- createdDirs.add(outPath);
148
- }
149
- break;
150
- }
151
- case "file":
152
- await pipeline(Readable.fromWeb(entry.body), createWriteStream(outPath, { mode: options.fmode ?? header.mode }));
153
- break;
154
- case "symlink":
155
- if (header.linkname) {
156
- if (validateSymlinks) {
157
- const symlinkDir = path.dirname(outPath);
158
- const resolvedTarget = path.resolve(symlinkDir, header.linkname);
159
- if (!resolvedTarget.startsWith(resolvedDestDir + path.sep) && resolvedTarget !== resolvedDestDir) throw new Error(`Symlink target "${header.linkname}" points outside of the extraction directory.`);
160
- }
161
- await fs.symlink(header.linkname, outPath);
162
- }
163
- break;
164
- case "link":
165
- if (header.linkname) await fs.link(path.join(directoryPath, header.linkname), outPath);
166
- break;
167
- }
168
- if (header.mtime) try {
169
- await (header.type === "symlink" ? fs.lutimes : fs.utimes)(outPath, header.mtime, header.mtime);
170
- } catch {}
233
+ const readable = new ReadableStream({
234
+ start(controller) {
235
+ bridge.on("data", (chunk) => controller.enqueue(chunk));
236
+ bridge.on("end", () => controller.close());
237
+ bridge.on("error", (err) => controller.error(err));
238
+ },
239
+ cancel(reason) {
240
+ bridge.destroy(reason instanceof Error ? reason : new Error(String(reason)));
171
241
  }
172
- })();
173
- return new Writable({
242
+ });
243
+ const writable = new Writable({
174
244
  write(chunk, encoding, callback) {
175
245
  if (!bridge.write(chunk, encoding)) {
176
246
  bridge.once("drain", callback);
@@ -187,7 +257,70 @@ function unpackTar(directoryPath, options = {}) {
187
257
  callback(err);
188
258
  }
189
259
  });
260
+ const processingPromise = (async () => {
261
+ const resolvedDestDir = path.resolve(directoryPath);
262
+ const createdDirs = /* @__PURE__ */ new Set();
263
+ await fs.mkdir(resolvedDestDir, { recursive: true });
264
+ createdDirs.add(resolvedDestDir);
265
+ const reader = readable.pipeThrough(createTarDecoder()).pipeThrough(createTarOptionsTransformer(options)).getReader();
266
+ try {
267
+ while (true) {
268
+ const { done, value: entry } = await reader.read();
269
+ if (done) break;
270
+ const { header } = entry;
271
+ if (path.isAbsolute(header.name)) throw new Error(`Path traversal attempt detected for entry "${header.name}".`);
272
+ const outPath = path.join(resolvedDestDir, header.name);
273
+ if (!outPath.startsWith(resolvedDestDir)) throw new Error(`Path traversal attempt detected for entry "${header.name}".`);
274
+ const parentDir = path.dirname(outPath);
275
+ if (!createdDirs.has(parentDir)) {
276
+ await fs.mkdir(parentDir, { recursive: true });
277
+ createdDirs.add(parentDir);
278
+ }
279
+ switch (header.type) {
280
+ case "directory": {
281
+ const mode = options.dmode ?? header.mode;
282
+ if (createdDirs.has(outPath) && mode) await fs.chmod(outPath, mode);
283
+ else {
284
+ await fs.mkdir(outPath, {
285
+ recursive: true,
286
+ mode
287
+ });
288
+ createdDirs.add(outPath);
289
+ }
290
+ break;
291
+ }
292
+ case "file":
293
+ await pipeline(Readable.fromWeb(entry.body), createWriteStream(outPath, { mode: options.fmode ?? header.mode }));
294
+ break;
295
+ case "symlink":
296
+ if (!header.linkname) break;
297
+ if (options.validateSymlinks ?? true) {
298
+ const symlinkDir = path.dirname(outPath);
299
+ if (!path.resolve(symlinkDir, header.linkname).startsWith(resolvedDestDir)) throw new Error(`Symlink target "${header.linkname}" points outside the extraction directory.`);
300
+ }
301
+ await fs.symlink(header.linkname, outPath);
302
+ break;
303
+ case "link": {
304
+ if (!header.linkname) break;
305
+ const resolvedLinkTarget = path.resolve(resolvedDestDir, header.linkname);
306
+ if (!resolvedLinkTarget.startsWith(resolvedDestDir)) throw new Error(`Hardlink target "${header.linkname}" points outside the extraction directory.`);
307
+ await fs.link(resolvedLinkTarget, outPath);
308
+ break;
309
+ }
310
+ }
311
+ if (header.mtime) try {
312
+ await (header.type === "symlink" ? fs.lutimes : fs.utimes)(outPath, header.mtime, header.mtime);
313
+ } catch {}
314
+ }
315
+ } finally {
316
+ reader.releaseLock();
317
+ }
318
+ })();
319
+ processingPromise.catch((err) => {
320
+ writable.destroy(err);
321
+ });
322
+ return writable;
190
323
  }
191
324
 
192
325
  //#endregion
193
- export { packTar, unpackTar };
326
+ export { packTar, packTarSources, unpackTar };
@@ -1,3 +1,73 @@
1
+ //#region src/web/compression.d.ts
2
+ /**
3
+ * Creates a gzip compression stream using the native
4
+ * [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) API.
5
+ *
6
+ * @returns A [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) configured for gzip compression
7
+ * @example
8
+ * ```typescript
9
+ * import { createGzipEncoder, createTarPacker } from '@modern-tar/core';
10
+ *
11
+ * // Create and compress a tar archive
12
+ * const { readable, controller } = createTarPacker();
13
+ * const compressedStream = readable.pipeThrough(createGzipEncoder());
14
+ *
15
+ * // Add entries...
16
+ * const fileStream = controller.add({ name: "file.txt", size: 5, type: "file" });
17
+ * const writer = fileStream.getWriter();
18
+ * await writer.write(new TextEncoder().encode("hello"));
19
+ * await writer.close();
20
+ * controller.finalize();
21
+ *
22
+ * // Upload compressed .tar.gz
23
+ * await fetch('/api/upload', {
24
+ * method: 'POST',
25
+ * body: compressedStream,
26
+ * headers: { 'Content-Type': 'application/gzip' }
27
+ * });
28
+ * ```
29
+ */
30
+ declare function createGzipEncoder(): CompressionStream;
31
+ /**
32
+ * Creates a gzip decompression stream using the native
33
+ * [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) API.
34
+ *
35
+ * @returns A [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) configured for gzip decompression
36
+ * @example
37
+ * ```typescript
38
+ * import { createGzipDecoder, createTarDecoder } from '@modern-tar/core';
39
+ *
40
+ * // Download and process a .tar.gz file
41
+ * const response = await fetch('https://api.example.com/archive.tar.gz');
42
+ * if (!response.body) throw new Error('No response body');
43
+ *
44
+ * // Chain decompression and tar parsing
45
+ * const entries = response.body
46
+ * .pipeThrough(createGzipDecoder())
47
+ * .pipeThrough(createTarDecoder());
48
+ *
49
+ * for await (const entry of entries) {
50
+ * console.log(`Extracted: ${entry.header.name}`);
51
+ * // Process entry.body ReadableStream as needed
52
+ * }
53
+ * ```
54
+ * @example
55
+ * ```typescript
56
+ * // Decompress local .tar.gz data
57
+ * const gzippedData = new Uint8Array([...]); // your gzipped tar data
58
+ * const stream = new ReadableStream({
59
+ * start(controller) {
60
+ * controller.enqueue(gzippedData);
61
+ * controller.close();
62
+ * }
63
+ * });
64
+ *
65
+ * const tarStream = stream.pipeThrough(createGzipDecoder());
66
+ * // Now process tarStream with createTarDecoder()...
67
+ * ```
68
+ */
69
+ declare function createGzipDecoder(): DecompressionStream;
70
+ //#endregion
1
71
  //#region src/web/types.d.ts
2
72
  /**
3
73
  * Header information for a tar entry in USTAR format.
@@ -78,76 +148,6 @@ interface UnpackOptions {
78
148
  map?: (header: TarHeader) => TarHeader;
79
149
  }
80
150
  //#endregion
81
- //#region src/web/compression.d.ts
82
- /**
83
- * Creates a gzip compression stream using the native
84
- * [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) API.
85
- *
86
- * @returns A [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) configured for gzip compression
87
- * @example
88
- * ```typescript
89
- * import { createGzipEncoder, createTarPacker } from '@modern-tar/core';
90
- *
91
- * // Create and compress a tar archive
92
- * const { readable, controller } = createTarPacker();
93
- * const compressedStream = readable.pipeThrough(createGzipEncoder());
94
- *
95
- * // Add entries...
96
- * const fileStream = controller.add({ name: "file.txt", size: 5, type: "file" });
97
- * const writer = fileStream.getWriter();
98
- * await writer.write(new TextEncoder().encode("hello"));
99
- * await writer.close();
100
- * controller.finalize();
101
- *
102
- * // Upload compressed .tar.gz
103
- * await fetch('/api/upload', {
104
- * method: 'POST',
105
- * body: compressedStream,
106
- * headers: { 'Content-Type': 'application/gzip' }
107
- * });
108
- * ```
109
- */
110
- declare function createGzipEncoder(): CompressionStream;
111
- /**
112
- * Creates a gzip decompression stream using the native
113
- * [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) API.
114
- *
115
- * @returns A [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) configured for gzip decompression
116
- * @example
117
- * ```typescript
118
- * import { createGzipDecoder, createTarDecoder } from '@modern-tar/core';
119
- *
120
- * // Download and process a .tar.gz file
121
- * const response = await fetch('https://api.example.com/archive.tar.gz');
122
- * if (!response.body) throw new Error('No response body');
123
- *
124
- * // Chain decompression and tar parsing
125
- * const entries = response.body
126
- * .pipeThrough(createGzipDecoder())
127
- * .pipeThrough(createTarDecoder());
128
- *
129
- * for await (const entry of entries) {
130
- * console.log(`Extracted: ${entry.header.name}`);
131
- * // Process entry.body ReadableStream as needed
132
- * }
133
- * ```
134
- * @example
135
- * ```typescript
136
- * // Decompress local .tar.gz data
137
- * const gzippedData = new Uint8Array([...]); // your gzipped tar data
138
- * const stream = new ReadableStream({
139
- * start(controller) {
140
- * controller.enqueue(gzippedData);
141
- * controller.close();
142
- * }
143
- * });
144
- *
145
- * const tarStream = stream.pipeThrough(createGzipDecoder());
146
- * // Now process tarStream with createTarDecoder()...
147
- * ```
148
- */
149
- declare function createGzipDecoder(): DecompressionStream;
150
- //#endregion
151
151
  //#region src/web/helpers.d.ts
152
152
  /**
153
153
  * Packs an array of tar entries into a single `Uint8Array` buffer.
@@ -1,2 +1,2 @@
1
- import { ParsedTarEntry, ParsedTarEntryWithData, TarEntry, TarEntryData, TarHeader, TarPackController, UnpackOptions, createGzipDecoder, createGzipEncoder, createTarDecoder, createTarOptionsTransformer, createTarPacker, packTar, unpackTar } from "../index-BLp7i3zL.js";
1
+ import { ParsedTarEntry, ParsedTarEntryWithData, TarEntry, TarEntryData, TarHeader, TarPackController, UnpackOptions, createGzipDecoder, createGzipEncoder, createTarDecoder, createTarOptionsTransformer, createTarPacker, packTar, unpackTar } from "../index-Dx4tbuJh.js";
2
2
  export { ParsedTarEntry, ParsedTarEntryWithData, TarEntry, TarEntryData, TarHeader, TarPackController, UnpackOptions, createGzipDecoder, createGzipEncoder, createTarDecoder, createTarOptionsTransformer, createTarPacker, packTar, unpackTar };
package/dist/web/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { createGzipDecoder, createGzipEncoder, createTarDecoder, createTarOptionsTransformer, createTarPacker, packTar, unpackTar } from "../web-BsjPG7md.js";
1
+ import { createGzipDecoder, createGzipEncoder, createTarDecoder, createTarOptionsTransformer, createTarPacker, packTar, unpackTar } from "../web-C1b5fAZt.js";
2
2
 
3
3
  export { createGzipDecoder, createGzipEncoder, createTarDecoder, createTarOptionsTransformer, createTarPacker, packTar, unpackTar };
@@ -1,3 +1,154 @@
1
+ //#region src/web/compression.ts
2
+ /**
3
+ * Creates a gzip compression stream using the native
4
+ * [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) API.
5
+ *
6
+ * @returns A [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) configured for gzip compression
7
+ * @example
8
+ * ```typescript
9
+ * import { createGzipEncoder, createTarPacker } from '@modern-tar/core';
10
+ *
11
+ * // Create and compress a tar archive
12
+ * const { readable, controller } = createTarPacker();
13
+ * const compressedStream = readable.pipeThrough(createGzipEncoder());
14
+ *
15
+ * // Add entries...
16
+ * const fileStream = controller.add({ name: "file.txt", size: 5, type: "file" });
17
+ * const writer = fileStream.getWriter();
18
+ * await writer.write(new TextEncoder().encode("hello"));
19
+ * await writer.close();
20
+ * controller.finalize();
21
+ *
22
+ * // Upload compressed .tar.gz
23
+ * await fetch('/api/upload', {
24
+ * method: 'POST',
25
+ * body: compressedStream,
26
+ * headers: { 'Content-Type': 'application/gzip' }
27
+ * });
28
+ * ```
29
+ */
30
+ function createGzipEncoder() {
31
+ return new CompressionStream("gzip");
32
+ }
33
+ /**
34
+ * Creates a gzip decompression stream using the native
35
+ * [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) API.
36
+ *
37
+ * @returns A [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) configured for gzip decompression
38
+ * @example
39
+ * ```typescript
40
+ * import { createGzipDecoder, createTarDecoder } from '@modern-tar/core';
41
+ *
42
+ * // Download and process a .tar.gz file
43
+ * const response = await fetch('https://api.example.com/archive.tar.gz');
44
+ * if (!response.body) throw new Error('No response body');
45
+ *
46
+ * // Chain decompression and tar parsing
47
+ * const entries = response.body
48
+ * .pipeThrough(createGzipDecoder())
49
+ * .pipeThrough(createTarDecoder());
50
+ *
51
+ * for await (const entry of entries) {
52
+ * console.log(`Extracted: ${entry.header.name}`);
53
+ * // Process entry.body ReadableStream as needed
54
+ * }
55
+ * ```
56
+ * @example
57
+ * ```typescript
58
+ * // Decompress local .tar.gz data
59
+ * const gzippedData = new Uint8Array([...]); // your gzipped tar data
60
+ * const stream = new ReadableStream({
61
+ * start(controller) {
62
+ * controller.enqueue(gzippedData);
63
+ * controller.close();
64
+ * }
65
+ * });
66
+ *
67
+ * const tarStream = stream.pipeThrough(createGzipDecoder());
68
+ * // Now process tarStream with createTarDecoder()...
69
+ * ```
70
+ */
71
+ function createGzipDecoder() {
72
+ return new DecompressionStream("gzip");
73
+ }
74
+
75
+ //#endregion
76
+ //#region src/web/options.ts
77
+ /**
78
+ * Creates a transform stream that applies {@link UnpackOptions} to tar entries.
79
+ *
80
+ * @param options - The unpacking options to apply
81
+ * @returns A TransformStream that processes {@link ParsedTarEntry} objects
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * import { createTarDecoder, createTarOptionsTransformer } from '@modern-tar/core';
86
+ *
87
+ * const transformedStream = sourceStream
88
+ * .pipeThrough(createTarDecoder())
89
+ * .pipeThrough(createTarOptionsTransformer({
90
+ * strip: 1,
91
+ * filter: (header) => header.name.endsWith('.txt'),
92
+ * map: (header) => ({ ...header, mode: 0o644 })
93
+ * }));
94
+ * ```
95
+ */
96
+ function createTarOptionsTransformer(options = {}) {
97
+ return new TransformStream({ async transform(entry, controller) {
98
+ let header = entry.header;
99
+ if (options.strip !== void 0) {
100
+ if (options.strip < 0) {
101
+ drainStream(entry.body);
102
+ throw new Error(`Invalid strip value: ${options.strip}. Must be non-negative.`);
103
+ }
104
+ if (options.strip > 0) {
105
+ const strippedComponents = header.name.split("/").filter((component) => component.length > 0).slice(options.strip);
106
+ if (strippedComponents.length === 0) {
107
+ drainStream(entry.body);
108
+ return;
109
+ }
110
+ const strippedName = strippedComponents.join("/");
111
+ if (header.type === "directory" && !strippedName.endsWith("/")) header = {
112
+ ...header,
113
+ name: `${strippedName}/`
114
+ };
115
+ else header = {
116
+ ...header,
117
+ name: strippedName
118
+ };
119
+ }
120
+ }
121
+ if (options.filter && options.filter(header) === false) {
122
+ drainStream(entry.body);
123
+ return;
124
+ }
125
+ if (options.map) header = options.map(header);
126
+ controller.enqueue({
127
+ header,
128
+ body: entry.body
129
+ });
130
+ } });
131
+ }
132
+ /**
133
+ * Drains the stream asynchronously without blocking the transform stream.
134
+ */
135
+ function drainStream(stream) {
136
+ (async () => {
137
+ const reader = stream.getReader();
138
+ try {
139
+ while (true) {
140
+ const { done } = await reader.read();
141
+ if (done) break;
142
+ }
143
+ } catch (error) {
144
+ console.debug("Stream drain error (non-critical):", error);
145
+ } finally {
146
+ reader.releaseLock();
147
+ }
148
+ })();
149
+ }
150
+
151
+ //#endregion
1
152
  //#region src/web/constants.ts
2
153
  /** Size of a TAR block in bytes. */
3
154
  const BLOCK_SIZE = 512;
@@ -227,6 +378,32 @@ function createTarPacker() {
227
378
  controller: {
228
379
  add(header) {
229
380
  const size = header.type === "directory" || header.type === "symlink" || header.type === "link" ? 0 : header.size ?? 0;
381
+ if (header.pax) {
382
+ let paxRecords = "";
383
+ for (const [key, value] of Object.entries(header.pax)) {
384
+ const record = `${key}=${value}\n`;
385
+ let length = record.length + 1;
386
+ const lengthStr = String(length);
387
+ length += lengthStr.length;
388
+ const finalLengthStr = String(length);
389
+ if (finalLengthStr.length !== lengthStr.length) length += finalLengthStr.length - lengthStr.length;
390
+ paxRecords += `${length} ${record}`;
391
+ }
392
+ if (paxRecords) {
393
+ const paxBytes = encoder.encode(paxRecords);
394
+ const paxHeader = createTarHeader({
395
+ name: `PaxHeader/${header.name}`,
396
+ size: paxBytes.length,
397
+ type: "pax-header",
398
+ mode: 420,
399
+ mtime: header.mtime
400
+ });
401
+ streamController.enqueue(paxHeader);
402
+ streamController.enqueue(paxBytes);
403
+ const paxPadding = (BLOCK_SIZE - paxBytes.length % BLOCK_SIZE) % BLOCK_SIZE;
404
+ if (paxPadding > 0) streamController.enqueue(new Uint8Array(paxPadding));
405
+ }
406
+ }
230
407
  const headerBlock = createTarHeader({
231
408
  ...header,
232
409
  size
@@ -268,157 +445,6 @@ function createTarPacker() {
268
445
  };
269
446
  }
270
447
 
271
- //#endregion
272
- //#region src/web/compression.ts
273
- /**
274
- * Creates a gzip compression stream using the native
275
- * [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) API.
276
- *
277
- * @returns A [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) configured for gzip compression
278
- * @example
279
- * ```typescript
280
- * import { createGzipEncoder, createTarPacker } from '@modern-tar/core';
281
- *
282
- * // Create and compress a tar archive
283
- * const { readable, controller } = createTarPacker();
284
- * const compressedStream = readable.pipeThrough(createGzipEncoder());
285
- *
286
- * // Add entries...
287
- * const fileStream = controller.add({ name: "file.txt", size: 5, type: "file" });
288
- * const writer = fileStream.getWriter();
289
- * await writer.write(new TextEncoder().encode("hello"));
290
- * await writer.close();
291
- * controller.finalize();
292
- *
293
- * // Upload compressed .tar.gz
294
- * await fetch('/api/upload', {
295
- * method: 'POST',
296
- * body: compressedStream,
297
- * headers: { 'Content-Type': 'application/gzip' }
298
- * });
299
- * ```
300
- */
301
- function createGzipEncoder() {
302
- return new CompressionStream("gzip");
303
- }
304
- /**
305
- * Creates a gzip decompression stream using the native
306
- * [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) API.
307
- *
308
- * @returns A [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) configured for gzip decompression
309
- * @example
310
- * ```typescript
311
- * import { createGzipDecoder, createTarDecoder } from '@modern-tar/core';
312
- *
313
- * // Download and process a .tar.gz file
314
- * const response = await fetch('https://api.example.com/archive.tar.gz');
315
- * if (!response.body) throw new Error('No response body');
316
- *
317
- * // Chain decompression and tar parsing
318
- * const entries = response.body
319
- * .pipeThrough(createGzipDecoder())
320
- * .pipeThrough(createTarDecoder());
321
- *
322
- * for await (const entry of entries) {
323
- * console.log(`Extracted: ${entry.header.name}`);
324
- * // Process entry.body ReadableStream as needed
325
- * }
326
- * ```
327
- * @example
328
- * ```typescript
329
- * // Decompress local .tar.gz data
330
- * const gzippedData = new Uint8Array([...]); // your gzipped tar data
331
- * const stream = new ReadableStream({
332
- * start(controller) {
333
- * controller.enqueue(gzippedData);
334
- * controller.close();
335
- * }
336
- * });
337
- *
338
- * const tarStream = stream.pipeThrough(createGzipDecoder());
339
- * // Now process tarStream with createTarDecoder()...
340
- * ```
341
- */
342
- function createGzipDecoder() {
343
- return new DecompressionStream("gzip");
344
- }
345
-
346
- //#endregion
347
- //#region src/web/options.ts
348
- /**
349
- * Creates a transform stream that applies {@link UnpackOptions} to tar entries.
350
- *
351
- * @param options - The unpacking options to apply
352
- * @returns A TransformStream that processes {@link ParsedTarEntry} objects
353
- *
354
- * @example
355
- * ```typescript
356
- * import { createTarDecoder, createTarOptionsTransformer } from '@modern-tar/core';
357
- *
358
- * const transformedStream = sourceStream
359
- * .pipeThrough(createTarDecoder())
360
- * .pipeThrough(createTarOptionsTransformer({
361
- * strip: 1,
362
- * filter: (header) => header.name.endsWith('.txt'),
363
- * map: (header) => ({ ...header, mode: 0o644 })
364
- * }));
365
- * ```
366
- */
367
- function createTarOptionsTransformer(options = {}) {
368
- return new TransformStream({ async transform(entry, controller) {
369
- let header = entry.header;
370
- if (options.strip !== void 0) {
371
- if (options.strip < 0) {
372
- drainStream(entry.body);
373
- throw new Error(`Invalid strip value: ${options.strip}. Must be non-negative.`);
374
- }
375
- if (options.strip > 0) {
376
- const strippedComponents = header.name.split("/").filter((component) => component.length > 0).slice(options.strip);
377
- if (strippedComponents.length === 0) {
378
- drainStream(entry.body);
379
- return;
380
- }
381
- const strippedName = strippedComponents.join("/");
382
- if (header.type === "directory" && !strippedName.endsWith("/")) header = {
383
- ...header,
384
- name: `${strippedName}/`
385
- };
386
- else header = {
387
- ...header,
388
- name: strippedName
389
- };
390
- }
391
- }
392
- if (options.filter && options.filter(header) === false) {
393
- drainStream(entry.body);
394
- return;
395
- }
396
- if (options.map) header = options.map(header);
397
- controller.enqueue({
398
- header,
399
- body: entry.body
400
- });
401
- } });
402
- }
403
- /**
404
- * Drains the stream asynchronously without blocking the transform stream.
405
- */
406
- function drainStream(stream) {
407
- (async () => {
408
- const reader = stream.getReader();
409
- try {
410
- while (true) {
411
- const { done } = await reader.read();
412
- if (done) break;
413
- }
414
- } catch (error) {
415
- console.debug("Stream drain error (non-critical):", error);
416
- } finally {
417
- reader.releaseLock();
418
- }
419
- })();
420
- }
421
-
422
448
  //#endregion
423
449
  //#region src/web/stream.ts
424
450
  function parseHeader(block) {
@@ -551,7 +577,11 @@ function createTarDecoder() {
551
577
  buffer = combined.subarray(offset);
552
578
  },
553
579
  flush(controller) {
554
- if (currentEntry) controller.error(/* @__PURE__ */ new Error("Tar archive is truncated."));
580
+ if (currentEntry) {
581
+ const error = /* @__PURE__ */ new Error("Tar archive is truncated.");
582
+ currentEntry.controller.error(error);
583
+ controller.error(error);
584
+ }
555
585
  if (buffer.some((b) => b !== 0)) controller.error(/* @__PURE__ */ new Error("Unexpected data at end of archive."));
556
586
  }
557
587
  });
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "modern-tar",
3
- "version": "0.0.0",
3
+ "version": "0.2.0",
4
4
  "description": "Zero dependency streaming tar parser and writer for JavaScript.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
+ "main": "./dist/web/index.js",
9
+ "module": "./dist/web/index.js",
10
+ "types": "./dist/web/index.d.ts",
8
11
  "exports": {
9
12
  "./package.json": "./package.json",
10
13
  ".": {