modern-tar 0.5.0 → 0.5.2
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.d.ts +21 -16
- package/dist/fs/index.js +116 -89
- package/package.json +1 -1
package/dist/fs/index.d.ts
CHANGED
|
@@ -50,43 +50,48 @@ interface UnpackOptionsFS extends UnpackOptions {
|
|
|
50
50
|
*/
|
|
51
51
|
concurrency?: number;
|
|
52
52
|
}
|
|
53
|
+
/** Base interface containing common metadata properties for all source types. */
|
|
54
|
+
interface BaseSource {
|
|
55
|
+
/** Destination path for the entry inside the tar archive. */
|
|
56
|
+
target: string;
|
|
57
|
+
/** Optional modification time. Overrides filesystem values or defaults to current time. */
|
|
58
|
+
mtime?: Date;
|
|
59
|
+
/** Optional user ID. Overrides filesystem values or defaults to 0. */
|
|
60
|
+
uid?: number;
|
|
61
|
+
/** Optional group ID. Overrides filesystem values or defaults to 0. */
|
|
62
|
+
gid?: number;
|
|
63
|
+
/** Optional user name. */
|
|
64
|
+
uname?: string;
|
|
65
|
+
/** Optional group name. */
|
|
66
|
+
gname?: string;
|
|
67
|
+
/** Optional Unix file permissions for the entry (e.g., 0o644, 0o755). */
|
|
68
|
+
mode?: number;
|
|
69
|
+
}
|
|
53
70
|
/** Describes a file on the local filesystem to be added to the archive. */
|
|
54
|
-
interface FileSource {
|
|
71
|
+
interface FileSource extends BaseSource {
|
|
55
72
|
type: "file";
|
|
56
73
|
/** Path to the source file on the local filesystem. */
|
|
57
74
|
source: string;
|
|
58
|
-
/** Destination path for the file inside the tar archive. */
|
|
59
|
-
target: string;
|
|
60
75
|
}
|
|
61
76
|
/** Describes a directory on the local filesystem to be added to the archive. */
|
|
62
|
-
interface DirectorySource {
|
|
77
|
+
interface DirectorySource extends BaseSource {
|
|
63
78
|
type: "directory";
|
|
64
79
|
/** Path to the source directory on the local filesystem. */
|
|
65
80
|
source: string;
|
|
66
|
-
/** Destination path for the directory inside the tar archive. */
|
|
67
|
-
target: string;
|
|
68
81
|
}
|
|
69
82
|
/** Describes raw, buffered content to be added to the archive. */
|
|
70
|
-
interface ContentSource {
|
|
83
|
+
interface ContentSource extends BaseSource {
|
|
71
84
|
type: "content";
|
|
72
85
|
/** Raw content to add. Supports string, Uint8Array, ArrayBuffer, Blob, or null. */
|
|
73
86
|
content: TarEntryData;
|
|
74
|
-
/** Destination path for the content inside the tar archive. */
|
|
75
|
-
target: string;
|
|
76
|
-
/** Optional Unix file permissions for the entry (e.g., 0o644). */
|
|
77
|
-
mode?: number;
|
|
78
87
|
}
|
|
79
88
|
/** Describes a stream of content to be added to the archive. */
|
|
80
|
-
interface StreamSource {
|
|
89
|
+
interface StreamSource extends BaseSource {
|
|
81
90
|
type: "stream";
|
|
82
91
|
/** A Readable or ReadableStream. */
|
|
83
92
|
content: Readable | ReadableStream;
|
|
84
|
-
/** Destination path for the content inside the tar archive. */
|
|
85
|
-
target: string;
|
|
86
93
|
/** The total size of the stream's content in bytes. This is required for streams. */
|
|
87
94
|
size: number;
|
|
88
|
-
/** Optional Unix file permissions for the entry (e.g., 0o644). */
|
|
89
|
-
mode?: number;
|
|
90
95
|
}
|
|
91
96
|
/** A union of all possible source types for creating a tar archive. */
|
|
92
97
|
type TarSource = FileSource | DirectorySource | ContentSource | StreamSource;
|
package/dist/fs/index.js
CHANGED
|
@@ -28,7 +28,8 @@ function packTar(sources, options = {}) {
|
|
|
28
28
|
let activeWorkers = 0;
|
|
29
29
|
let allJobsQueued = false;
|
|
30
30
|
const writer = async () => {
|
|
31
|
-
const
|
|
31
|
+
const readBufferSmall = Buffer.alloc(64 * 1024);
|
|
32
|
+
let readBufferLarge = null;
|
|
32
33
|
while (true) {
|
|
33
34
|
if (stream.destroyed) return;
|
|
34
35
|
if (allJobsQueued && writeIndex >= jobs.length) break;
|
|
@@ -57,6 +58,7 @@ function packTar(sources, options = {}) {
|
|
|
57
58
|
}
|
|
58
59
|
else {
|
|
59
60
|
const { handle, size } = result.body;
|
|
61
|
+
const readBuffer = size > 1048576 ? readBufferLarge ??= Buffer.alloc(512 * 1024) : readBufferSmall;
|
|
60
62
|
try {
|
|
61
63
|
let bytesLeft = size;
|
|
62
64
|
while (bytesLeft > 0 && !stream.destroyed) {
|
|
@@ -99,8 +101,9 @@ function packTar(sources, options = {}) {
|
|
|
99
101
|
if (job.type === "content" || job.type === "stream") {
|
|
100
102
|
let body$1;
|
|
101
103
|
let size;
|
|
104
|
+
const isDir$1 = target.endsWith("/");
|
|
102
105
|
if (job.type === "stream") {
|
|
103
|
-
if (typeof job.size !== "number" || job.size <= 0) throw new Error("
|
|
106
|
+
if (typeof job.size !== "number" || !isDir$1 && job.size <= 0 || isDir$1 && job.size !== 0) throw new Error(isDir$1 ? "Streams for directories must have size 0." : "Streams require a positive size.");
|
|
104
107
|
size = job.size;
|
|
105
108
|
body$1 = job.content;
|
|
106
109
|
} else {
|
|
@@ -109,29 +112,31 @@ function packTar(sources, options = {}) {
|
|
|
109
112
|
body$1 = content;
|
|
110
113
|
}
|
|
111
114
|
const stat$1 = {
|
|
112
|
-
size,
|
|
113
|
-
isFile: () =>
|
|
114
|
-
isDirectory: () =>
|
|
115
|
+
size: isDir$1 ? 0 : size,
|
|
116
|
+
isFile: () => !isDir$1,
|
|
117
|
+
isDirectory: () => isDir$1,
|
|
115
118
|
isSymbolicLink: () => false,
|
|
116
|
-
mode: job.mode
|
|
117
|
-
mtime: /* @__PURE__ */ new Date(),
|
|
118
|
-
uid:
|
|
119
|
-
gid:
|
|
119
|
+
mode: job.mode,
|
|
120
|
+
mtime: job.mtime ?? /* @__PURE__ */ new Date(),
|
|
121
|
+
uid: job.uid ?? 0,
|
|
122
|
+
gid: job.gid ?? 0
|
|
120
123
|
};
|
|
121
124
|
if (filter && !filter(target, stat$1)) return;
|
|
122
125
|
let header$1 = {
|
|
123
126
|
name: target,
|
|
124
|
-
type: "file",
|
|
125
|
-
size,
|
|
127
|
+
type: isDir$1 ? "directory" : "file",
|
|
128
|
+
size: isDir$1 ? 0 : size,
|
|
126
129
|
mode: stat$1.mode,
|
|
127
130
|
mtime: stat$1.mtime,
|
|
128
131
|
uid: stat$1.uid,
|
|
129
|
-
gid: stat$1.gid
|
|
132
|
+
gid: stat$1.gid,
|
|
133
|
+
uname: job.uname,
|
|
134
|
+
gname: job.gname
|
|
130
135
|
};
|
|
131
136
|
if (map) header$1 = map(header$1);
|
|
132
137
|
jobResult = {
|
|
133
138
|
header: header$1,
|
|
134
|
-
body: body$1
|
|
139
|
+
body: isDir$1 ? void 0 : body$1
|
|
135
140
|
};
|
|
136
141
|
return;
|
|
137
142
|
}
|
|
@@ -147,10 +152,12 @@ function packTar(sources, options = {}) {
|
|
|
147
152
|
let header = {
|
|
148
153
|
name: target,
|
|
149
154
|
size: 0,
|
|
150
|
-
mode: Number(stat.mode),
|
|
151
|
-
mtime: stat.mtime,
|
|
152
|
-
uid: Number(stat.uid),
|
|
153
|
-
gid: Number(stat.gid),
|
|
155
|
+
mode: job.mode ?? Number(stat.mode),
|
|
156
|
+
mtime: job.mtime ?? stat.mtime,
|
|
157
|
+
uid: job.uid ?? Number(stat.uid),
|
|
158
|
+
gid: job.gid ?? Number(stat.gid),
|
|
159
|
+
uname: job.uname,
|
|
160
|
+
gname: job.gname,
|
|
154
161
|
type: "file"
|
|
155
162
|
};
|
|
156
163
|
let body;
|
|
@@ -199,22 +206,46 @@ function packTar(sources, options = {}) {
|
|
|
199
206
|
return stream;
|
|
200
207
|
}
|
|
201
208
|
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/fs/win-path.ts
|
|
211
|
+
const REPLACEMENTS = {
|
|
212
|
+
":": "",
|
|
213
|
+
"<": "",
|
|
214
|
+
">": "",
|
|
215
|
+
"|": "",
|
|
216
|
+
"?": "",
|
|
217
|
+
"*": "",
|
|
218
|
+
"\"": ""
|
|
219
|
+
};
|
|
220
|
+
function normalizeWindowsPath(p) {
|
|
221
|
+
if (!(process.platform === "win32")) return p;
|
|
222
|
+
const normalized = p.replace(/\\/g, "/");
|
|
223
|
+
if (/^[A-Za-z]:\.\./i.test(normalized)) throw new Error(`Entry ${p} points outside extraction directory.`);
|
|
224
|
+
return normalized.replace(/^[A-Za-z]:/, "").replace(/[<>:"|?*]/g, (char) => REPLACEMENTS[char]);
|
|
225
|
+
}
|
|
226
|
+
|
|
202
227
|
//#endregion
|
|
203
228
|
//#region src/fs/path.ts
|
|
204
229
|
const unicodeCache = /* @__PURE__ */ new Map();
|
|
205
|
-
const MAX_CACHE_SIZE = 1e4;
|
|
206
230
|
const normalizeUnicode = (s) => {
|
|
207
231
|
let result = unicodeCache.get(s);
|
|
208
232
|
if (result !== void 0) unicodeCache.delete(s);
|
|
209
233
|
result = result ?? s.normalize("NFD");
|
|
210
234
|
unicodeCache.set(s, result);
|
|
211
|
-
|
|
212
|
-
if (overflow > MAX_CACHE_SIZE / 10) {
|
|
213
|
-
const keys = unicodeCache.keys();
|
|
214
|
-
for (let i = 0; i < overflow; i++) unicodeCache.delete(keys.next().value);
|
|
215
|
-
}
|
|
235
|
+
if (unicodeCache.size > 1e4) unicodeCache.delete(unicodeCache.keys().next().value);
|
|
216
236
|
return result;
|
|
217
237
|
};
|
|
238
|
+
function stripTrailingSlashes(p) {
|
|
239
|
+
let i = p.length - 1;
|
|
240
|
+
if (i === -1 || p[i] !== "/") return p;
|
|
241
|
+
let slashesStart = i;
|
|
242
|
+
while (i > -1 && p[i] === "/") {
|
|
243
|
+
slashesStart = i;
|
|
244
|
+
i--;
|
|
245
|
+
}
|
|
246
|
+
return p.slice(0, slashesStart);
|
|
247
|
+
}
|
|
248
|
+
const normalizeHeaderName = (s) => normalizeUnicode(normalizeWindowsPath(stripTrailingSlashes(s)));
|
|
218
249
|
function validateBounds(targetPath, destDir, errorMessage) {
|
|
219
250
|
const target = normalizeUnicode(path.resolve(targetPath));
|
|
220
251
|
const dest = path.resolve(destDir);
|
|
@@ -309,9 +340,11 @@ function createFSHandler(directoryPath, options) {
|
|
|
309
340
|
let promise = pathPromises.get(dirPath);
|
|
310
341
|
if (promise) return promise;
|
|
311
342
|
promise = (async () => {
|
|
343
|
+
if (signal.aborted) throw signal.reason;
|
|
312
344
|
const destDir = await destDirPromise;
|
|
313
345
|
if (dirPath === destDir.symbolic) return "directory";
|
|
314
346
|
await ensureDirectoryExists(path.dirname(dirPath));
|
|
347
|
+
if (signal.aborted) throw signal.reason;
|
|
315
348
|
try {
|
|
316
349
|
await fs.mkdir(dirPath, { mode: dmode });
|
|
317
350
|
return "directory";
|
|
@@ -330,63 +363,6 @@ function createFSHandler(directoryPath, options) {
|
|
|
330
363
|
pathPromises.set(dirPath, promise);
|
|
331
364
|
return promise;
|
|
332
365
|
};
|
|
333
|
-
const processHeader = async (header, entryStream) => {
|
|
334
|
-
try {
|
|
335
|
-
const destDir = await destDirPromise;
|
|
336
|
-
const normalizedName = normalizeUnicode(header.name);
|
|
337
|
-
if (maxDepth !== Infinity && normalizedName.split("/").length > maxDepth) throw new Error("Tar exceeds max specified depth.");
|
|
338
|
-
if (path.isAbsolute(normalizedName)) throw new Error(`Absolute path found in "${header.name}".`);
|
|
339
|
-
const outPath = path.join(destDir.symbolic, normalizedName);
|
|
340
|
-
validateBounds(outPath, destDir.symbolic, `Entry "${header.name}" points outside the extraction directory.`);
|
|
341
|
-
const parentDir = path.dirname(outPath);
|
|
342
|
-
await ensureDirectoryExists(parentDir);
|
|
343
|
-
switch (header.type) {
|
|
344
|
-
case "directory":
|
|
345
|
-
await fs.mkdir(outPath, {
|
|
346
|
-
recursive: true,
|
|
347
|
-
mode: dmode ?? header.mode
|
|
348
|
-
});
|
|
349
|
-
break;
|
|
350
|
-
case "file": {
|
|
351
|
-
const fileStream = createWriteStream(outPath, {
|
|
352
|
-
mode: fmode ?? header.mode,
|
|
353
|
-
highWaterMark: header.size > 1048576 ? 524288 : void 0
|
|
354
|
-
});
|
|
355
|
-
await pipeline(entryStream, fileStream);
|
|
356
|
-
break;
|
|
357
|
-
}
|
|
358
|
-
case "symlink": {
|
|
359
|
-
const { linkname } = header;
|
|
360
|
-
if (!linkname) return header.type;
|
|
361
|
-
const target = path.resolve(parentDir, linkname);
|
|
362
|
-
validateBounds(target, destDir.symbolic, `Symlink "${linkname}" points outside the extraction directory.`);
|
|
363
|
-
await fs.symlink(linkname, outPath);
|
|
364
|
-
break;
|
|
365
|
-
}
|
|
366
|
-
case "link": {
|
|
367
|
-
const { linkname } = header;
|
|
368
|
-
if (!linkname) return header.type;
|
|
369
|
-
const normalizedLink = normalizeUnicode(linkname);
|
|
370
|
-
if (path.isAbsolute(normalizedLink)) throw new Error(`Hardlink "${linkname}" points outside the extraction directory.`);
|
|
371
|
-
const linkTarget = path.join(destDir.symbolic, normalizedLink);
|
|
372
|
-
validateBounds(linkTarget, destDir.symbolic, `Hardlink "${linkname}" points outside the extraction directory.`);
|
|
373
|
-
await ensureDirectoryExists(path.dirname(linkTarget));
|
|
374
|
-
const realTargetParent = await fs.realpath(path.dirname(linkTarget));
|
|
375
|
-
const realLinkTarget = path.join(realTargetParent, path.basename(linkTarget));
|
|
376
|
-
validateBounds(realLinkTarget, destDir.real, `Hardlink "${linkname}" points outside the extraction directory.`);
|
|
377
|
-
const targetPromise = pathPromises.get(linkTarget);
|
|
378
|
-
if (targetPromise) await targetPromise;
|
|
379
|
-
await fs.link(linkTarget, outPath);
|
|
380
|
-
break;
|
|
381
|
-
}
|
|
382
|
-
default: return header.type;
|
|
383
|
-
}
|
|
384
|
-
if (header.mtime) await (header.type === "symlink" ? fs.lutimes : fs.utimes)(outPath, header.mtime, header.mtime).catch(() => {});
|
|
385
|
-
return header.type;
|
|
386
|
-
} finally {
|
|
387
|
-
if (!entryStream.readableEnded) entryStream.resume();
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
366
|
return {
|
|
391
367
|
handler: {
|
|
392
368
|
onHeader(header) {
|
|
@@ -403,21 +379,73 @@ function createFSHandler(directoryPath, options) {
|
|
|
403
379
|
processQueue();
|
|
404
380
|
return;
|
|
405
381
|
}
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
opPromise = (pathPromises.get(normalizedTarget) || Promise.resolve(void 0)).then(async (priorOp) => {
|
|
382
|
+
const name = normalizeHeaderName(transformed.name);
|
|
383
|
+
const target = path.join(path.resolve(directoryPath), name);
|
|
384
|
+
opPromise = (pathPromises.get(target) || Promise.resolve(void 0)).then(async (priorOp) => {
|
|
410
385
|
if (signal.aborted) throw signal.reason;
|
|
411
386
|
if (priorOp) {
|
|
412
|
-
if (priorOp === "directory" && transformed.type !== "directory" || priorOp !== "directory" && transformed.type === "directory") throw new Error(`Path conflict
|
|
387
|
+
if (priorOp === "directory" && transformed.type !== "directory" || priorOp !== "directory" && transformed.type === "directory") throw new Error(`Path conflict ${transformed.type} over existing ${priorOp} at "${transformed.name}"`);
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const destDir = await destDirPromise;
|
|
391
|
+
if (maxDepth !== Infinity && name.split("/").length > maxDepth) throw new Error("Tar exceeds max specified depth.");
|
|
392
|
+
if (path.isAbsolute(name)) throw new Error(`Absolute path found in "${transformed.name}".`);
|
|
393
|
+
const outPath = path.join(destDir.symbolic, name);
|
|
394
|
+
validateBounds(outPath, destDir.symbolic, `Entry "${transformed.name}" points outside the extraction directory.`);
|
|
395
|
+
const parentDir = path.dirname(outPath);
|
|
396
|
+
await ensureDirectoryExists(parentDir);
|
|
397
|
+
switch (transformed.type) {
|
|
398
|
+
case "directory":
|
|
399
|
+
await fs.mkdir(outPath, {
|
|
400
|
+
recursive: true,
|
|
401
|
+
mode: dmode ?? transformed.mode
|
|
402
|
+
});
|
|
403
|
+
break;
|
|
404
|
+
case "file": {
|
|
405
|
+
const fileStream = createWriteStream(outPath, {
|
|
406
|
+
mode: fmode ?? transformed.mode,
|
|
407
|
+
highWaterMark: transformed.size > 1048576 ? 524288 : void 0
|
|
408
|
+
});
|
|
409
|
+
await pipeline(entryStream, fileStream);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case "symlink": {
|
|
413
|
+
const { linkname } = transformed;
|
|
414
|
+
if (!linkname) return transformed.type;
|
|
415
|
+
const target$1 = path.resolve(parentDir, linkname);
|
|
416
|
+
validateBounds(target$1, destDir.symbolic, `Symlink "${linkname}" points outside the extraction directory.`);
|
|
417
|
+
await fs.symlink(linkname, outPath);
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case "link": {
|
|
421
|
+
const { linkname } = transformed;
|
|
422
|
+
if (!linkname) return transformed.type;
|
|
423
|
+
const normalizedLink = normalizeUnicode(linkname);
|
|
424
|
+
if (path.isAbsolute(normalizedLink)) throw new Error(`Hardlink "${linkname}" points outside the extraction directory.`);
|
|
425
|
+
const linkTarget = path.join(destDir.symbolic, normalizedLink);
|
|
426
|
+
validateBounds(linkTarget, destDir.symbolic, `Hardlink "${linkname}" points outside the extraction directory.`);
|
|
427
|
+
await ensureDirectoryExists(path.dirname(linkTarget));
|
|
428
|
+
const realTargetParent = await fs.realpath(path.dirname(linkTarget));
|
|
429
|
+
const realLinkTarget = path.join(realTargetParent, path.basename(linkTarget));
|
|
430
|
+
validateBounds(realLinkTarget, destDir.real, `Hardlink "${linkname}" points outside the extraction directory.`);
|
|
431
|
+
if (linkTarget === outPath) return transformed.type;
|
|
432
|
+
const targetPromise = pathPromises.get(linkTarget);
|
|
433
|
+
if (targetPromise) await targetPromise;
|
|
434
|
+
await fs.link(linkTarget, outPath);
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
default: return transformed.type;
|
|
438
|
+
}
|
|
439
|
+
if (transformed.mtime) await (transformed.type === "symlink" ? fs.lutimes : fs.utimes)(outPath, transformed.mtime, transformed.mtime).catch(() => {});
|
|
440
|
+
return transformed.type;
|
|
441
|
+
} finally {
|
|
442
|
+
if (!entryStream.readableEnded) entryStream.resume();
|
|
413
443
|
}
|
|
414
|
-
return await processHeader(transformed, entryStream);
|
|
415
444
|
});
|
|
416
|
-
pathPromises.set(
|
|
445
|
+
pathPromises.set(target, opPromise);
|
|
417
446
|
} catch (err) {
|
|
418
447
|
opPromise = Promise.reject(err);
|
|
419
448
|
abortController.abort(err);
|
|
420
|
-
entryStream?.destroy(err);
|
|
421
449
|
}
|
|
422
450
|
opPromise.catch((err) => abortController.abort(err)).finally(() => {
|
|
423
451
|
activeOps--;
|
|
@@ -436,7 +464,6 @@ function createFSHandler(directoryPath, options) {
|
|
|
436
464
|
},
|
|
437
465
|
onError(error) {
|
|
438
466
|
abortController.abort(error);
|
|
439
|
-
activeEntryStream?.destroy(error);
|
|
440
467
|
},
|
|
441
468
|
async process() {
|
|
442
469
|
processingEnded = true;
|