chore: 更新学生档案数据
This commit is contained in:
21
node_modules/modern-tar/LICENSE
generated
vendored
Normal file
21
node_modules/modern-tar/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Ayuhito
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
277
node_modules/modern-tar/README.md
generated
vendored
Normal file
277
node_modules/modern-tar/README.md
generated
vendored
Normal file
@@ -0,0 +1,277 @@
|
||||
# 🗄️ modern-tar
|
||||
|
||||
Zero-dependency, cross-platform, streaming tar archive library for every JavaScript runtime. Built with the browser-native Web Streams API for performance and memory efficiency.
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 **Streaming Architecture** - Supports large archives without loading everything into memory.
|
||||
- 📋 **Standards Compliant** - Full USTAR format support with PAX extensions. Compatible with GNU tar, BSD tar, and other standard implementations.
|
||||
- 🗜️ **Compression** - Includes helpers for gzip compression/decompression.
|
||||
- 📝 **TypeScript First** - Full type safety with detailed TypeDoc documentation.
|
||||
- ⚡ **Zero Dependencies** - No external dependencies, minimal bundle size.
|
||||
- 🌐 **Cross-Platform** - Works in browsers, Node.js, Cloudflare Workers, and other JavaScript runtimes.
|
||||
- 📁 **Node.js Integration** - Additional high-level APIs for directory packing and extraction.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install modern-tar
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
This package provides two entry points:
|
||||
|
||||
- `modern-tar`: The core, cross-platform streaming API (works everywhere).
|
||||
- `modern-tar/fs`: High-level filesystem utilities for Node.js.
|
||||
|
||||
### Core Usage
|
||||
|
||||
These APIs use the Web Streams API and can be used in any modern JavaScript environment.
|
||||
|
||||
#### Simple
|
||||
|
||||
```typescript
|
||||
import { packTar, unpackTar } from 'modern-tar';
|
||||
|
||||
// Pack entries into a tar buffer
|
||||
const entries = [
|
||||
{ header: { name: "file.txt", size: 5 }, body: "hello" },
|
||||
{ header: { name: "dir/", type: "directory", size: 0 } },
|
||||
{ header: { name: "dir/nested.txt", size: 3 }, body: new Uint8Array([97, 98, 99]) } // "abc"
|
||||
];
|
||||
|
||||
// Accepts string, Uint8Array, Blob, ReadableStream<Uint8Array> and more...
|
||||
const tarBuffer = await packTar(entries);
|
||||
|
||||
// Unpack tar buffer into entries
|
||||
const entries = await unpackTar(tarBuffer);
|
||||
for (const entry of entries) {
|
||||
console.log(`File: ${entry.header.name}`);
|
||||
const content = new TextDecoder().decode(entry.data);
|
||||
console.log(`Content: ${content}`);
|
||||
}
|
||||
```
|
||||
|
||||
#### Streaming
|
||||
|
||||
```typescript
|
||||
import { createTarPacker, createTarDecoder } from 'modern-tar';
|
||||
|
||||
// Create a tar packer
|
||||
const { readable, controller } = createTarPacker();
|
||||
|
||||
// Add entries dynamically
|
||||
const fileStream = controller.add({
|
||||
name: "dynamic.txt",
|
||||
size: 5,
|
||||
type: "file"
|
||||
});
|
||||
|
||||
// Write content to the stream
|
||||
const writer = fileStream.getWriter();
|
||||
await writer.write(new TextEncoder().encode("hello"));
|
||||
await writer.close();
|
||||
|
||||
// When done adding entries, finalize the archive
|
||||
controller.finalize();
|
||||
|
||||
// Pipe the archive right into a decoder
|
||||
const decodedStream = readable.pipeThrough(createTarDecoder());
|
||||
for await (const entry of decodedStream) {
|
||||
console.log(`Decoded: ${entry.header.name}`);
|
||||
|
||||
const shouldSkip = entry.header.name.endsWith(".md");
|
||||
if (shouldSkip) {
|
||||
// You MUST drain the body with cancel() to proceed to the next entry or read it fully,
|
||||
// otherwise the stream will stall.
|
||||
await entry.body.cancel();
|
||||
continue;
|
||||
}
|
||||
|
||||
const reader = entry.body.getReader();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
processChunk(value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Compression/Decompression (gzip)
|
||||
|
||||
```typescript
|
||||
import { createGzipEncoder, createTarPacker } from 'modern-tar';
|
||||
|
||||
// Create and compress a tar archive
|
||||
const { readable, controller } = createTarPacker();
|
||||
const compressedStream = readable.pipeThrough(createGzipEncoder());
|
||||
|
||||
// Add entries...
|
||||
const fileStream = controller.add({ name: "file.txt", size: 5, type: "file" });
|
||||
const writer = fileStream.getWriter();
|
||||
await writer.write(new TextEncoder().encode("hello"));
|
||||
await writer.close();
|
||||
controller.finalize();
|
||||
|
||||
// Upload compressed .tar.gz
|
||||
await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: compressedStream,
|
||||
headers: { 'Content-Type': 'application/gzip' }
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { createGzipDecoder, createTarDecoder, unpackTar } from 'modern-tar';
|
||||
|
||||
// Download and process a .tar.gz file
|
||||
const response = await fetch('https://api.example.com/archive.tar.gz');
|
||||
if (!response.body) throw new Error('No response body');
|
||||
|
||||
// Buffer entire archive
|
||||
const entries = await unpackTar(response.body.pipeThrough(createGzipDecoder()));
|
||||
|
||||
for (const entry of entries) {
|
||||
console.log(`Extracted: ${entry.header.name}`);
|
||||
const content = new TextDecoder().decode(entry.data);
|
||||
console.log(`Content: ${content}`);
|
||||
}
|
||||
|
||||
// Or chain decompression and tar parsing using streams
|
||||
const entries = response.body
|
||||
.pipeThrough(createGzipDecoder())
|
||||
.pipeThrough(createTarDecoder());
|
||||
|
||||
for await (const entry of entries) {
|
||||
console.log(`Extracted: ${entry.header.name}`);
|
||||
// Process entry.body ReadableStream as needed
|
||||
}
|
||||
```
|
||||
|
||||
### Node.js Filesystem Usage
|
||||
|
||||
These APIs use Node.js streams when interacting with the local filesystem.
|
||||
|
||||
#### Simple
|
||||
|
||||
```typescript
|
||||
import { packTar, unpackTar } from 'modern-tar/fs';
|
||||
import { createWriteStream, createReadStream } from 'node:fs';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
|
||||
// Pack a directory into a tar file
|
||||
const tarStream = packTar('./my/project');
|
||||
const fileStream = createWriteStream('./project.tar');
|
||||
await pipeline(tarStream, fileStream);
|
||||
|
||||
// Extract a tar file to a directory
|
||||
const tarReadStream = createReadStream('./project.tar', {
|
||||
highWaterMark: 256 * 1024 // 256 KB for optimal performance
|
||||
});
|
||||
const extractStream = unpackTar('./output/directory');
|
||||
await pipeline(tarReadStream, extractStream);
|
||||
```
|
||||
|
||||
#### Filtering and Transformation
|
||||
|
||||
```typescript
|
||||
import { packTar, unpackTar } from 'modern-tar/fs';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
|
||||
// Pack with filtering
|
||||
const packStream = packTar('./my/project', {
|
||||
filter: (filePath, stats) => !filePath.includes('node_modules'),
|
||||
map: (header) => ({ ...header, mode: 0o644 }), // Set all files to 644
|
||||
dereference: true // Follow symlinks instead of archiving them
|
||||
});
|
||||
|
||||
// Unpack with advanced options
|
||||
const sourceStream = createReadStream('./archive.tar', {
|
||||
highWaterMark: 256 * 1024 // 256 KB for optimal performance
|
||||
});
|
||||
const extractStream = unpackTar('./output', {
|
||||
// Core options
|
||||
strip: 1, // Remove first directory level
|
||||
filter: (header) => header.name.endsWith('.js'), // Only extract JS files
|
||||
map: (header) => ({ ...header, name: header.name.toLowerCase() }), // Transform names
|
||||
|
||||
// Filesystem-specific options
|
||||
fmode: 0o644, // Override file permissions
|
||||
dmode: 0o755, // Override directory permissions
|
||||
maxDepth: 50, // Limit extraction depth for security (default: 1024)
|
||||
concurrency: 8 // Limit concurrent filesystem operations (default: CPU cores)
|
||||
});
|
||||
|
||||
await pipeline(sourceStream, extractStream);
|
||||
```
|
||||
|
||||
#### Archive Creation
|
||||
|
||||
```typescript
|
||||
import { packTar, type TarSource } from 'modern-tar/fs';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
|
||||
// Pack multiple sources
|
||||
const sources: TarSource[] = [
|
||||
{ type: 'file', source: './package.json', target: 'project/package.json' },
|
||||
{ type: 'directory', source: './src', target: 'project/src' },
|
||||
{ type: 'content', content: 'Hello World!', target: 'project/hello.txt' },
|
||||
{ type: 'content', content: '#!/bin/bash\necho "Executable"', target: 'bin/script.sh', mode: 0o755 },
|
||||
{ type: 'stream', content: createReadStream('./large-file.bin'), target: 'project/data.bin', size: 1048576 },
|
||||
{ type: 'stream', content: fetch('/api/data').then(r => r.body!), target: 'project/remote.json', size: 2048 }
|
||||
];
|
||||
|
||||
const archiveStream = packTar(sources);
|
||||
await pipeline(archiveStream, createWriteStream('project.tar'));
|
||||
```
|
||||
|
||||
#### Compression/Decompression (gzip)
|
||||
|
||||
```typescript
|
||||
import { packTar, unpackTar } from 'modern-tar/fs';
|
||||
import { createWriteStream, createReadStream } from 'node:fs';
|
||||
import { createGzip, createGunzip } from 'node:zlib';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
|
||||
// Pack directory and compress to .tar.gz
|
||||
const tarStream = packTar('./my/project');
|
||||
await pipeline(tarStream, createGzip(), createWriteStream('./project.tar.gz'));
|
||||
|
||||
// Decompress and extract .tar.gz
|
||||
const gzipStream = createReadStream('./project.tar.gz', {
|
||||
highWaterMark: 256 * 1024 // 256 KB for optimal performance
|
||||
});
|
||||
await pipeline(gzipStream, createGunzip(), unpackTar('./output'));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
See the [API Reference](./REFERENCE.md).
|
||||
|
||||
# Benchmarks
|
||||
|
||||
Current benchmarks indicate we're much faster than other popular tar libraries for small file archives (packing and unpacking). On the other hand, larger files hit an I/O bottleneck resulting in similar performance between libraries.
|
||||
|
||||
See the [Results](./benchmarks/README.md).
|
||||
|
||||
## Compatibility
|
||||
|
||||
The core library uses the [Web Streams API](https://caniuse.com/streams) and requires:
|
||||
|
||||
- **Node.js**: 18.0+
|
||||
- **Browsers**: Modern browsers with Web Streams support
|
||||
- Chrome 71+
|
||||
- Firefox 102+
|
||||
- Safari 14.1+
|
||||
- Edge 79+
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [`tar-stream`](https://github.com/mafintosh/tar-stream) and [`tar-fs`](https://github.com/mafintosh/tar-fs) - For the inspiration and test fixtures.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
175
node_modules/modern-tar/dist/fs/index.d.ts
generated
vendored
Normal file
175
node_modules/modern-tar/dist/fs/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
import { i as UnpackOptions, n as TarEntryData, r as TarHeader } from "../types-D19dF2SE.js";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import { Stats } from "node:fs";
|
||||
|
||||
//#region src/fs/types.d.ts
|
||||
/**
|
||||
* Filesystem-specific configuration options for packing directories into tar archives.
|
||||
*
|
||||
* These options are specific to Node.js filesystem operations and use Node.js-specific
|
||||
* types like `Stats` for file system metadata.
|
||||
*/
|
||||
interface PackOptionsFS {
|
||||
/** Follow symlinks instead of storing them as symlinks (default: false) */
|
||||
dereference?: boolean;
|
||||
/** Filter function to include/exclude files (return false to exclude) */
|
||||
filter?: (path: string, stat: Stats) => boolean;
|
||||
/** Transform function to modify tar headers before packing */
|
||||
map?: (header: TarHeader) => TarHeader;
|
||||
/** Base directory for symlink security validation, when `dereference` is set to true. */
|
||||
baseDir?: string;
|
||||
/**
|
||||
* Maximum number of concurrent filesystem operations during packing.
|
||||
* @default os.cpus().length || 8
|
||||
*/
|
||||
concurrency?: number;
|
||||
}
|
||||
/**
|
||||
* Filesystem-specific configuration options for extracting tar archives to the filesystem.
|
||||
*
|
||||
* Extends the core {@link UnpackOptions} with Node.js filesystem-specific settings
|
||||
* for controlling file permissions and other filesystem behaviors.
|
||||
*/
|
||||
interface UnpackOptionsFS extends UnpackOptions {
|
||||
/** Default mode for created directories (e.g., 0o755). If not specified, uses mode from tar header or system default */
|
||||
dmode?: number;
|
||||
/** Default mode for created files (e.g., 0o644). If not specified, uses mode from tar header or system default */
|
||||
fmode?: number;
|
||||
/**
|
||||
* The maximum depth of paths to extract. Prevents Denial of Service (DoS) attacks
|
||||
* from malicious archives with deeply nested directories.
|
||||
*
|
||||
* Set to `Infinity` to disable depth checking (not recommended for untrusted archives).
|
||||
* @default 1024
|
||||
*/
|
||||
maxDepth?: number;
|
||||
/**
|
||||
* Maximum number of concurrent filesystem operations during extraction.
|
||||
* @default os.cpus().length || 8
|
||||
*/
|
||||
concurrency?: number;
|
||||
}
|
||||
/** Base interface containing common metadata properties for all source types. */
|
||||
interface BaseSource {
|
||||
/** Destination path for the entry inside the tar archive. */
|
||||
target: string;
|
||||
/** Optional modification time. Overrides filesystem values or defaults to current time. */
|
||||
mtime?: Date;
|
||||
/** Optional user ID. Overrides filesystem values or defaults to 0. */
|
||||
uid?: number;
|
||||
/** Optional group ID. Overrides filesystem values or defaults to 0. */
|
||||
gid?: number;
|
||||
/** Optional user name. */
|
||||
uname?: string;
|
||||
/** Optional group name. */
|
||||
gname?: string;
|
||||
/** Optional Unix file permissions for the entry (e.g., 0o644, 0o755). */
|
||||
mode?: number;
|
||||
}
|
||||
/** Describes a file on the local filesystem to be added to the archive. */
|
||||
interface FileSource extends BaseSource {
|
||||
type: "file";
|
||||
/** Path to the source file on the local filesystem. */
|
||||
source: string;
|
||||
}
|
||||
/** Describes a directory on the local filesystem to be added to the archive. */
|
||||
interface DirectorySource extends BaseSource {
|
||||
type: "directory";
|
||||
/** Path to the source directory on the local filesystem. */
|
||||
source: string;
|
||||
}
|
||||
/** Describes raw, buffered content to be added to the archive. */
|
||||
interface ContentSource extends BaseSource {
|
||||
type: "content";
|
||||
/** Raw content to add. Supports string, Uint8Array, ArrayBuffer, Blob, or null. */
|
||||
content: TarEntryData;
|
||||
}
|
||||
/** Describes a stream of content to be added to the archive. */
|
||||
interface StreamSource extends BaseSource {
|
||||
type: "stream";
|
||||
/** A Readable or ReadableStream. */
|
||||
content: Readable | ReadableStream;
|
||||
/** The total size of the stream's content in bytes. This is required for streams. */
|
||||
size: number;
|
||||
}
|
||||
/** A union of all possible source types for creating a tar archive. */
|
||||
type TarSource = FileSource | DirectorySource | ContentSource | StreamSource;
|
||||
//#endregion
|
||||
//#region src/fs/pack.d.ts
|
||||
/**
|
||||
* @deprecated Use `packTar` instead. This function is now an alias for `packTar`.
|
||||
*/
|
||||
declare const packTarSources: typeof packTar;
|
||||
/**
|
||||
* Pack a directory or multiple sources into a Node.js `Readable` stream containing
|
||||
* tar archive bytes. Can pack either a single directory or an array of sources
|
||||
* (files, directories, or raw content).
|
||||
*
|
||||
* @param sources - Either a directory path string or an array of {@link TarSource} objects.
|
||||
* @param options - Optional packing configuration using {@link PackOptionsFS}.
|
||||
* @returns Node.js [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable) stream of tar archive bytes
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { packTar } from 'modern-tar/fs';
|
||||
* import { createWriteStream } from 'node:fs';
|
||||
* import { pipeline } from 'node:stream/promises';
|
||||
*
|
||||
* // Basic directory packing
|
||||
* const tarStream = packTar('/home/user/project');
|
||||
* await pipeline(tarStream, createWriteStream('project.tar'));
|
||||
*
|
||||
* // Pack multiple sources
|
||||
* const sources = [
|
||||
* { type: 'file', source: './package.json', target: 'project/package.json' },
|
||||
* { type: 'directory', source: './src', target: 'project/src' },
|
||||
* { type: 'content', content: 'hello world', target: 'project/hello.txt' }
|
||||
* ];
|
||||
* const archiveStream = packTar(sources);
|
||||
* await pipeline(archiveStream, createWriteStream('project.tar'));
|
||||
*
|
||||
* // With filtering and transformation
|
||||
* const filteredStream = packTar('/my/project', {
|
||||
* filter: (path, stats) => !path.includes('node_modules'),
|
||||
* map: (header) => ({ ...header, uname: 'builder' }),
|
||||
* dereference: true // Follow symlinks
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
declare function packTar(sources: TarSource[] | string, options?: PackOptionsFS): Readable;
|
||||
//#endregion
|
||||
//#region src/fs/unpack.d.ts
|
||||
/**
|
||||
* Extract a tar archive to a directory.
|
||||
*
|
||||
* Returns a Node.js [`Writable`](https://nodejs.org/api/stream.html#class-streamwritable)
|
||||
* stream to pipe tar archive bytes into. Files, directories, symlinks, and hardlinks
|
||||
* are written to the filesystem with correct permissions and timestamps.
|
||||
*
|
||||
* @param directoryPath - Path to directory where files will be extracted
|
||||
* @param options - Optional extraction configuration
|
||||
* @returns Node.js [`Writable`](https://nodejs.org/api/stream.html#class-streamwritable) stream to pipe tar archive bytes into
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { unpackTar } from 'modern-tar/fs';
|
||||
* import { createReadStream } from 'node:fs';
|
||||
* import { pipeline } from 'node:stream/promises';
|
||||
*
|
||||
* // Basic extraction
|
||||
* const tarStream = createReadStream('project.tar');
|
||||
* const extractStream = unpackTar('/output/directory');
|
||||
* await pipeline(tarStream, extractStream);
|
||||
*
|
||||
* // Extract with path manipulation and filtering
|
||||
* const advancedStream = unpackTar('/output', {
|
||||
* strip: 1, // Remove first path component
|
||||
* filter: (header) => header.type === 'file' && header.name.endsWith('.js'),
|
||||
* map: (header) => ({ ...header, mode: 0o644 })
|
||||
* });
|
||||
* await pipeline(createReadStream('archive.tar'), advancedStream);
|
||||
* ```
|
||||
*/
|
||||
declare function unpackTar(directoryPath: string, options?: UnpackOptionsFS): Writable;
|
||||
//#endregion
|
||||
export { type ContentSource, type DirectorySource, type FileSource, type PackOptionsFS, type TarSource, type UnpackOptionsFS, packTar, packTarSources, unpackTar };
|
||||
746
node_modules/modern-tar/dist/fs/index.js
generated
vendored
Normal file
746
node_modules/modern-tar/dist/fs/index.js
generated
vendored
Normal file
@@ -0,0 +1,746 @@
|
||||
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-CPCEF5CT.js";
|
||||
import * as fs$1 from "node:fs/promises";
|
||||
import { cpus } from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import * as fs from "node:fs";
|
||||
//#region src/fs/cache.ts
|
||||
const createCache = () => {
|
||||
const m = /* @__PURE__ */ new Map();
|
||||
return {
|
||||
get(k) {
|
||||
const v = m.get(k);
|
||||
if (m.delete(k)) m.set(k, v);
|
||||
return v;
|
||||
},
|
||||
set(k, v) {
|
||||
if (m.set(k, v).size > 1e4) m.delete(m.keys().next().value);
|
||||
}
|
||||
};
|
||||
};
|
||||
//#endregion
|
||||
//#region src/fs/path.ts
|
||||
const unicodeCache = createCache();
|
||||
const normalizeUnicode = (s) => {
|
||||
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) >= 128) {
|
||||
const cached = unicodeCache.get(s);
|
||||
if (cached !== void 0) return cached;
|
||||
const normalized = s.normalize("NFD");
|
||||
unicodeCache.set(s, normalized);
|
||||
return normalized;
|
||||
}
|
||||
return s;
|
||||
};
|
||||
function validateBounds(targetPath, destDir, errorMessage) {
|
||||
const target = normalizeUnicode(path.resolve(targetPath));
|
||||
const dest = path.resolve(destDir);
|
||||
if (target !== dest && !target.startsWith(dest + path.sep)) throw new Error(errorMessage);
|
||||
}
|
||||
const win32Reserved = {
|
||||
":": "",
|
||||
"<": "",
|
||||
">": "",
|
||||
"|": "",
|
||||
"?": "",
|
||||
"*": "",
|
||||
"\"": ""
|
||||
};
|
||||
function normalizeName(name) {
|
||||
const path = name.replace(/\\/g, "/");
|
||||
if (path.split("/").includes("..") || /^[a-zA-Z]:\.\./.test(path)) throw new Error(`${name} points outside extraction directory`);
|
||||
let relative = path;
|
||||
if (/^[a-zA-Z]:/.test(relative)) relative = relative.replace(/^[a-zA-Z]:[/\\]?/, "");
|
||||
else if (relative.startsWith("/")) relative = relative.replace(/^\/+/, "");
|
||||
if (process.platform === "win32") return relative.replace(/[<>:"|?*]/g, (char) => win32Reserved[char]);
|
||||
return relative;
|
||||
}
|
||||
const normalizeHeaderName = (s) => normalizeUnicode(normalizeName(s.replace(/\/+$/, "")));
|
||||
//#endregion
|
||||
//#region src/fs/pack.ts
|
||||
const packTarSources = packTar;
|
||||
function packTar(sources, options = {}) {
|
||||
const stream = new Readable({ read() {} });
|
||||
(async () => {
|
||||
const packer = createTarPacker((chunk) => stream.push(Buffer.from(chunk)), stream.destroy.bind(stream), () => stream.push(null));
|
||||
const { dereference = false, filter, map, baseDir, concurrency = cpus().length || 8 } = options;
|
||||
const isDir = typeof sources === "string";
|
||||
const directoryPath = isDir ? path.resolve(sources) : null;
|
||||
const jobs = isDir ? (await fs$1.readdir(directoryPath, { withFileTypes: true })).map((entry) => ({
|
||||
type: entry.isDirectory() ? DIRECTORY : FILE,
|
||||
source: path.join(directoryPath, entry.name),
|
||||
target: entry.name
|
||||
})) : sources;
|
||||
const results = /* @__PURE__ */ new Map();
|
||||
const resolvers = /* @__PURE__ */ new Map();
|
||||
const seenInodes = /* @__PURE__ */ new Map();
|
||||
let jobIndex = 0;
|
||||
let writeIndex = 0;
|
||||
let activeWorkers = 0;
|
||||
let allJobsQueued = false;
|
||||
const writer = async () => {
|
||||
const readBufferSmall = Buffer.alloc(64 * 1024);
|
||||
let readBufferLarge = null;
|
||||
while (true) {
|
||||
if (stream.destroyed) return;
|
||||
if (allJobsQueued && writeIndex >= jobs.length) break;
|
||||
if (!results.has(writeIndex)) {
|
||||
await new Promise((resolve) => resolvers.set(writeIndex, resolve));
|
||||
continue;
|
||||
}
|
||||
const result = results.get(writeIndex);
|
||||
results.delete(writeIndex);
|
||||
resolvers.delete(writeIndex);
|
||||
if (!result) {
|
||||
writeIndex++;
|
||||
continue;
|
||||
}
|
||||
packer.add(result.header);
|
||||
if (result.body) if (result.body instanceof Uint8Array) {
|
||||
if (result.body.length > 0) packer.write(result.body);
|
||||
} else if (result.body instanceof Readable || result.body instanceof ReadableStream) try {
|
||||
for await (const chunk of result.body) {
|
||||
if (stream.destroyed) break;
|
||||
packer.write(chunk instanceof Uint8Array ? chunk : Buffer.from(chunk));
|
||||
}
|
||||
} catch (error) {
|
||||
stream.destroy(error);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
const { handle, size } = result.body;
|
||||
const readBuffer = size > 1048576 ? readBufferLarge ??= Buffer.alloc(512 * 1024) : readBufferSmall;
|
||||
try {
|
||||
let bytesLeft = size;
|
||||
while (bytesLeft > 0 && !stream.destroyed) {
|
||||
const toRead = Math.min(bytesLeft, readBuffer.length);
|
||||
const { bytesRead } = await handle.read(readBuffer, 0, toRead, null);
|
||||
if (bytesRead === 0) break;
|
||||
packer.write(readBuffer.subarray(0, bytesRead));
|
||||
bytesLeft -= bytesRead;
|
||||
}
|
||||
} catch (error) {
|
||||
stream.destroy(error);
|
||||
return;
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
}
|
||||
packer.endEntry();
|
||||
writeIndex++;
|
||||
}
|
||||
};
|
||||
const controller = () => {
|
||||
if (stream.destroyed || allJobsQueued) return;
|
||||
while (activeWorkers < concurrency && jobIndex < jobs.length) {
|
||||
activeWorkers++;
|
||||
const currentIndex = jobIndex++;
|
||||
processJob(jobs[currentIndex], currentIndex).catch(stream.destroy.bind(stream)).finally(() => {
|
||||
activeWorkers--;
|
||||
controller();
|
||||
});
|
||||
}
|
||||
if (activeWorkers === 0 && jobIndex >= jobs.length) {
|
||||
allJobsQueued = true;
|
||||
resolvers.get(writeIndex)?.();
|
||||
}
|
||||
};
|
||||
const processJob = async (job, index) => {
|
||||
let jobResult = null;
|
||||
const target = normalizeName(job.target);
|
||||
try {
|
||||
if (job.type === "content" || job.type === "stream") {
|
||||
let body;
|
||||
let size;
|
||||
const isDir = target.endsWith("/");
|
||||
if (job.type === "stream") {
|
||||
if (!isDir && job.size <= 0 || isDir && job.size !== 0) throw new Error(isDir ? "Streams for directories must have size 0." : "Streams require a positive size.");
|
||||
size = job.size;
|
||||
body = job.content;
|
||||
} else {
|
||||
const content = await normalizeBody(job.content);
|
||||
size = content.length;
|
||||
body = content;
|
||||
}
|
||||
const stat = {
|
||||
size: isDir ? 0 : size,
|
||||
isFile: () => !isDir,
|
||||
isDirectory: () => isDir,
|
||||
isSymbolicLink: () => false,
|
||||
mode: job.mode,
|
||||
mtime: job.mtime ?? /* @__PURE__ */ new Date(),
|
||||
uid: job.uid ?? 0,
|
||||
gid: job.gid ?? 0
|
||||
};
|
||||
if (filter && !filter(target, stat)) return;
|
||||
let header = {
|
||||
name: target,
|
||||
type: isDir ? DIRECTORY : FILE,
|
||||
size: isDir ? 0 : size,
|
||||
mode: stat.mode,
|
||||
mtime: stat.mtime,
|
||||
uid: stat.uid,
|
||||
gid: stat.gid,
|
||||
uname: job.uname,
|
||||
gname: job.gname
|
||||
};
|
||||
if (map) header = map(header);
|
||||
jobResult = {
|
||||
header,
|
||||
body: isDir ? void 0 : body
|
||||
};
|
||||
return;
|
||||
}
|
||||
let stat = await fs$1.lstat(job.source, { bigint: true });
|
||||
if (dereference && stat.isSymbolicLink()) {
|
||||
const linkTarget = await fs$1.readlink(job.source);
|
||||
const resolved = path.resolve(path.dirname(job.source), linkTarget);
|
||||
const resolvedBase = baseDir ?? directoryPath ?? process.cwd();
|
||||
if (!resolved.startsWith(resolvedBase + path.sep) && resolved !== resolvedBase) return;
|
||||
stat = await fs$1.stat(job.source, { bigint: true });
|
||||
}
|
||||
if (filter && !filter(job.source, stat)) return;
|
||||
let header = {
|
||||
name: target,
|
||||
size: 0,
|
||||
mode: job.mode ?? Number(stat.mode),
|
||||
mtime: job.mtime ?? stat.mtime,
|
||||
uid: job.uid ?? Number(stat.uid),
|
||||
gid: job.gid ?? Number(stat.gid),
|
||||
uname: job.uname,
|
||||
gname: job.gname,
|
||||
type: FILE
|
||||
};
|
||||
let body;
|
||||
if (stat.isDirectory()) {
|
||||
header.type = DIRECTORY;
|
||||
header.name = target.endsWith("/") ? target : `${target}/`;
|
||||
try {
|
||||
for (const d of await fs$1.readdir(job.source, { withFileTypes: true })) jobs.push({
|
||||
type: d.isDirectory() ? DIRECTORY : FILE,
|
||||
source: path.join(job.source, d.name),
|
||||
target: `${header.name}${d.name}`
|
||||
});
|
||||
} catch {}
|
||||
} else if (stat.isSymbolicLink()) {
|
||||
header.type = SYMLINK;
|
||||
header.linkname = await fs$1.readlink(job.source);
|
||||
} else if (stat.isFile()) {
|
||||
header.size = Number(stat.size);
|
||||
if (stat.nlink > 1 && seenInodes.has(stat.ino)) {
|
||||
header.type = LINK;
|
||||
header.linkname = seenInodes.get(stat.ino);
|
||||
header.size = 0;
|
||||
} else {
|
||||
if (stat.nlink > 1) seenInodes.set(stat.ino, target);
|
||||
if (header.size > 0) if (header.size < 32 * 1024) body = await fs$1.readFile(job.source);
|
||||
else body = {
|
||||
handle: await fs$1.open(job.source, "r"),
|
||||
size: header.size
|
||||
};
|
||||
}
|
||||
} else return;
|
||||
if (map) header = map(header);
|
||||
jobResult = {
|
||||
header,
|
||||
body
|
||||
};
|
||||
} finally {
|
||||
results.set(index, jobResult);
|
||||
resolvers.get(index)?.();
|
||||
}
|
||||
};
|
||||
controller();
|
||||
await writer();
|
||||
if (!stream.destroyed) packer.finalize();
|
||||
})().catch((error) => stream.destroy(error));
|
||||
return stream;
|
||||
}
|
||||
//#endregion
|
||||
//#region src/fs/concurrency.ts
|
||||
const createOperationQueue = (concurrency) => {
|
||||
let active = 0;
|
||||
const tasks = [];
|
||||
let head = 0;
|
||||
let idle = null;
|
||||
let resolveIdle = null;
|
||||
const ensureIdle = () => idle ??= new Promise((resolve) => resolveIdle = resolve);
|
||||
const flush = () => {
|
||||
while (active < concurrency && head < tasks.length) {
|
||||
const task = tasks[head++];
|
||||
active++;
|
||||
task().finally(() => {
|
||||
active--;
|
||||
flush();
|
||||
});
|
||||
}
|
||||
if (head === tasks.length) {
|
||||
tasks.length = 0;
|
||||
head = 0;
|
||||
if (active === 0 && resolveIdle) {
|
||||
resolveIdle();
|
||||
idle = null;
|
||||
resolveIdle = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
return {
|
||||
add(op) {
|
||||
const wasIdle = active === 0 && head === tasks.length;
|
||||
return new Promise((resolve, reject) => {
|
||||
tasks.push(() => Promise.resolve().then(op).then(resolve, reject));
|
||||
if (wasIdle) ensureIdle();
|
||||
flush();
|
||||
});
|
||||
},
|
||||
onIdle() {
|
||||
return active === 0 && head === tasks.length ? Promise.resolve() : ensureIdle();
|
||||
}
|
||||
};
|
||||
};
|
||||
//#endregion
|
||||
//#region src/fs/file-sink.ts
|
||||
const BATCH_BYTES = 256 * 1024;
|
||||
const OPEN_FLAGS = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW ?? 0);
|
||||
const STATE_UNOPENED = 0;
|
||||
const STATE_OPENING = 1;
|
||||
const STATE_OPEN = 2;
|
||||
const STATE_CLOSED = 3;
|
||||
const STATE_FAILED = 4;
|
||||
const DRAINED_PROMISE = Promise.resolve();
|
||||
function createFileSink(path, { mode = 438, mtime } = {}) {
|
||||
let state = STATE_UNOPENED;
|
||||
let flushing = false;
|
||||
let fd = null;
|
||||
let queue = [];
|
||||
let spare = [];
|
||||
let bytes = 0;
|
||||
let storedError = null;
|
||||
let endPromise = null;
|
||||
let endResolve = null;
|
||||
let endReject = null;
|
||||
const waitResolves = [];
|
||||
const waitRejects = [];
|
||||
const settleWaiters = () => {
|
||||
if (waitResolves.length === 0) return;
|
||||
for (let i = 0; i < waitResolves.length; i++) waitResolves[i]();
|
||||
waitResolves.length = 0;
|
||||
waitRejects.length = 0;
|
||||
};
|
||||
const failWaiters = (error) => {
|
||||
if (waitRejects.length === 0) return;
|
||||
for (let i = 0; i < waitRejects.length; i++) waitRejects[i](error);
|
||||
waitRejects.length = 0;
|
||||
waitResolves.length = 0;
|
||||
};
|
||||
const resetBuffers = () => {
|
||||
bytes = 0;
|
||||
queue.length = 0;
|
||||
spare.length = 0;
|
||||
};
|
||||
const finish = () => {
|
||||
state = STATE_CLOSED;
|
||||
endResolve?.();
|
||||
settleWaiters();
|
||||
};
|
||||
const swapQueues = () => {
|
||||
const current = queue;
|
||||
queue = spare;
|
||||
spare = current;
|
||||
queue.length = 0;
|
||||
return current;
|
||||
};
|
||||
const fail = (error) => {
|
||||
if (storedError) return;
|
||||
storedError = error;
|
||||
state = STATE_FAILED;
|
||||
resetBuffers();
|
||||
flushing = false;
|
||||
const fdToClose = fd;
|
||||
fd = null;
|
||||
if (fdToClose !== null) fs.ftruncate(fdToClose, 0, () => fs.close(fdToClose));
|
||||
endReject?.(error);
|
||||
failWaiters(error);
|
||||
};
|
||||
const close = () => {
|
||||
if (fd === null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
const fdToClose = fd;
|
||||
fd = null;
|
||||
if (mtime) fs.futimes(fdToClose, mtime, mtime, (err) => {
|
||||
if (err) return fail(err);
|
||||
fs.close(fdToClose, (closeErr) => {
|
||||
if (closeErr) fail(closeErr);
|
||||
else finish();
|
||||
});
|
||||
});
|
||||
else fs.close(fdToClose, (err) => {
|
||||
if (err) fail(err);
|
||||
else finish();
|
||||
});
|
||||
};
|
||||
const flush = () => {
|
||||
if (flushing || queue.length === 0 || state !== STATE_OPEN) return;
|
||||
flushing = true;
|
||||
const bufs = swapQueues();
|
||||
const onDone = (err, written = 0) => {
|
||||
if (err) return fail(err);
|
||||
flushing = false;
|
||||
bytes -= written;
|
||||
spare.length = 0;
|
||||
if (bytes < BATCH_BYTES) settleWaiters();
|
||||
if (queue.length > 0) flush();
|
||||
else if (endResolve) close();
|
||||
};
|
||||
if (bufs.length === 1) {
|
||||
const buf = bufs[0];
|
||||
fs.write(fd, buf, 0, buf.length, null, onDone);
|
||||
} else fs.writev(fd, bufs, onDone);
|
||||
};
|
||||
const open = () => {
|
||||
if (state !== STATE_UNOPENED) return;
|
||||
state = STATE_OPENING;
|
||||
fs.open(path, OPEN_FLAGS, mode, (err, openFd) => {
|
||||
if (err) return fail(err);
|
||||
if (state === STATE_CLOSED || state === STATE_FAILED) {
|
||||
fs.close(openFd);
|
||||
return;
|
||||
}
|
||||
fd = openFd;
|
||||
state = STATE_OPEN;
|
||||
if (endResolve) if (queue.length > 0) flush();
|
||||
else close();
|
||||
else if (bytes >= BATCH_BYTES && !flushing) flush();
|
||||
else settleWaiters();
|
||||
});
|
||||
};
|
||||
const write = (chunk) => {
|
||||
if (storedError || state >= STATE_CLOSED || endResolve) return false;
|
||||
if (state !== STATE_OPEN && state !== STATE_OPENING) open();
|
||||
const buf = Buffer.isBuffer(chunk) ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength) : Buffer.from(chunk);
|
||||
if (buf.length === 0) return bytes < BATCH_BYTES;
|
||||
queue.push(buf);
|
||||
bytes += buf.length;
|
||||
if (state === STATE_OPEN && !flushing && bytes >= BATCH_BYTES) flush();
|
||||
return bytes < BATCH_BYTES;
|
||||
};
|
||||
const waitDrain = () => {
|
||||
if (bytes < BATCH_BYTES || state !== STATE_OPEN) return DRAINED_PROMISE;
|
||||
return new Promise((resolve, reject) => {
|
||||
waitResolves.push(resolve);
|
||||
waitRejects.push(reject);
|
||||
});
|
||||
};
|
||||
const end = () => {
|
||||
if (state >= STATE_CLOSED) return DRAINED_PROMISE;
|
||||
if (storedError) return Promise.reject(storedError);
|
||||
if (endPromise) return endPromise;
|
||||
endPromise = new Promise((resolve, reject) => {
|
||||
endResolve = resolve;
|
||||
endReject = reject;
|
||||
if (state !== STATE_OPEN && state !== STATE_OPENING) open();
|
||||
else if (state === STATE_OPEN && !flushing) if (queue.length > 0) flush();
|
||||
else close();
|
||||
});
|
||||
return endPromise;
|
||||
};
|
||||
const destroy = (error) => {
|
||||
if (error) {
|
||||
fail(error);
|
||||
return;
|
||||
}
|
||||
if (state >= STATE_CLOSED || storedError) return;
|
||||
resetBuffers();
|
||||
flushing = false;
|
||||
if (fd !== null) {
|
||||
const fdToClose = fd;
|
||||
fd = null;
|
||||
fs.close(fdToClose);
|
||||
}
|
||||
finish();
|
||||
};
|
||||
return {
|
||||
write,
|
||||
end,
|
||||
destroy,
|
||||
waitDrain
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
//#region src/fs/path-cache.ts
|
||||
const ENOENT = "ENOENT";
|
||||
const createPathCache = (destDirPath, options) => {
|
||||
const { maxDepth = 1024, dmode } = options;
|
||||
const dirPromises = createCache();
|
||||
const pathConflicts = /* @__PURE__ */ new Map();
|
||||
const deferredLinks = [];
|
||||
const realDirCache = createCache();
|
||||
const initializeDestDir = async (destDirPath) => {
|
||||
const symbolic = normalizeUnicode(path.resolve(destDirPath));
|
||||
try {
|
||||
await fs$1.mkdir(symbolic, { recursive: true });
|
||||
} catch (err) {
|
||||
if (err.code === ENOENT) {
|
||||
const parentDir = path.dirname(symbolic);
|
||||
if (parentDir === symbolic) throw err;
|
||||
await fs$1.mkdir(parentDir, { recursive: true });
|
||||
await fs$1.mkdir(symbolic, { recursive: true });
|
||||
} else throw err;
|
||||
}
|
||||
try {
|
||||
return {
|
||||
symbolic,
|
||||
real: await fs$1.realpath(symbolic)
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.code === ENOENT) return {
|
||||
symbolic,
|
||||
real: symbolic
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
const destDirPromise = initializeDestDir(destDirPath);
|
||||
destDirPromise.catch(() => {});
|
||||
const getRealDir = async (dirPath, errorMessage) => {
|
||||
const destDir = await destDirPromise;
|
||||
if (dirPath === destDir.symbolic) {
|
||||
validateBounds(destDir.real, destDir.real, errorMessage);
|
||||
return destDir.real;
|
||||
}
|
||||
let promise = realDirCache.get(dirPath);
|
||||
if (!promise) {
|
||||
promise = fs$1.realpath(dirPath).then((realPath) => {
|
||||
validateBounds(realPath, destDir.real, errorMessage);
|
||||
return realPath;
|
||||
});
|
||||
realDirCache.set(dirPath, promise);
|
||||
}
|
||||
const realDir = await promise;
|
||||
validateBounds(realDir, destDir.real, errorMessage);
|
||||
return realDir;
|
||||
};
|
||||
const prepareDirectory = async (dirPath, mode) => {
|
||||
let promise = dirPromises.get(dirPath);
|
||||
if (promise) return promise;
|
||||
promise = (async () => {
|
||||
if (dirPath === (await destDirPromise).symbolic) return;
|
||||
await prepareDirectory(path.dirname(dirPath));
|
||||
try {
|
||||
const stat = await fs$1.lstat(dirPath);
|
||||
if (stat.isDirectory()) return;
|
||||
if (stat.isSymbolicLink()) try {
|
||||
const realPath = await getRealDir(dirPath, `Symlink "${dirPath}" points outside the extraction directory.`);
|
||||
if ((await fs$1.stat(realPath)).isDirectory()) return;
|
||||
} catch (err) {
|
||||
if (err.code === ENOENT) throw new Error(`Symlink "${dirPath}" points outside the extraction directory.`);
|
||||
throw err;
|
||||
}
|
||||
throw new Error(`"${dirPath}" is not a valid directory component.`);
|
||||
} catch (err) {
|
||||
if (err.code === ENOENT) {
|
||||
await fs$1.mkdir(dirPath, { mode: mode ?? options.dmode });
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
})();
|
||||
dirPromises.set(dirPath, promise);
|
||||
return promise;
|
||||
};
|
||||
return {
|
||||
async ready() {
|
||||
await destDirPromise;
|
||||
},
|
||||
async preparePath(header) {
|
||||
const { name, linkname, type, mode, mtime } = header;
|
||||
const normalizedName = normalizeHeaderName(name);
|
||||
const destDir = await destDirPromise;
|
||||
const outPath = path.join(destDir.symbolic, normalizedName);
|
||||
validateBounds(outPath, destDir.symbolic, `Entry "${name}" points outside the extraction directory.`);
|
||||
if (maxDepth !== Infinity) {
|
||||
let depth = 1;
|
||||
for (const char of normalizedName) if (char === "/" && ++depth > maxDepth) throw new Error("Tar exceeds max specified depth.");
|
||||
}
|
||||
const prevOp = pathConflicts.get(normalizedName);
|
||||
if (prevOp) {
|
||||
if (prevOp === "directory" && type !== "directory" || prevOp !== "directory" && type === "directory") throw new Error(`Path conflict ${type} over existing ${prevOp} at "${name}"`);
|
||||
return;
|
||||
}
|
||||
const parentDir = path.dirname(outPath);
|
||||
switch (type) {
|
||||
case DIRECTORY: {
|
||||
pathConflicts.set(normalizedName, DIRECTORY);
|
||||
const safeMode = mode ? mode & 511 : void 0;
|
||||
await prepareDirectory(outPath, dmode ?? safeMode);
|
||||
if (mtime) await fs$1.lutimes(outPath, mtime, mtime).catch(() => {});
|
||||
return;
|
||||
}
|
||||
case FILE:
|
||||
pathConflicts.set(normalizedName, FILE);
|
||||
await prepareDirectory(parentDir);
|
||||
return outPath;
|
||||
case SYMLINK:
|
||||
pathConflicts.set(normalizedName, SYMLINK);
|
||||
if (!linkname) return;
|
||||
await prepareDirectory(parentDir);
|
||||
validateBounds(path.resolve(parentDir, linkname), destDir.symbolic, `Symlink "${linkname}" points outside the extraction directory.`);
|
||||
await fs$1.symlink(linkname, outPath);
|
||||
if (mtime) await fs$1.lutimes(outPath, mtime, mtime).catch(() => {});
|
||||
return;
|
||||
case LINK: {
|
||||
pathConflicts.set(normalizedName, LINK);
|
||||
if (!linkname) return;
|
||||
const normalizedLink = normalizeUnicode(linkname);
|
||||
if (path.isAbsolute(normalizedLink)) throw new Error(`Hardlink "${linkname}" points outside the extraction directory.`);
|
||||
const linkTarget = path.join(destDir.symbolic, normalizedLink);
|
||||
validateBounds(linkTarget, destDir.symbolic, `Hardlink "${linkname}" points outside the extraction directory.`);
|
||||
await prepareDirectory(path.dirname(linkTarget));
|
||||
const realTargetParent = await getRealDir(path.dirname(linkTarget), `Hardlink "${linkname}" points outside the extraction directory.`);
|
||||
validateBounds(path.join(realTargetParent, path.basename(linkTarget)), destDir.real, `Hardlink "${linkname}" points outside the extraction directory.`);
|
||||
if (linkTarget !== outPath) {
|
||||
await prepareDirectory(parentDir);
|
||||
deferredLinks.push({
|
||||
linkTarget,
|
||||
outPath
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
default: return;
|
||||
}
|
||||
},
|
||||
async applyLinks() {
|
||||
for (const { linkTarget, outPath } of deferredLinks) try {
|
||||
await fs$1.link(linkTarget, outPath);
|
||||
} catch (err) {
|
||||
if (err.code === ENOENT) throw new Error(`Hardlink target "${linkTarget}" does not exist for link at "${outPath}".`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
//#endregion
|
||||
//#region src/fs/unpack.ts
|
||||
function unpackTar(directoryPath, options = {}) {
|
||||
const unpacker = createUnpacker(options);
|
||||
const opQueue = createOperationQueue(options.concurrency || cpus().length || 8);
|
||||
const pathCache = createPathCache(directoryPath, options);
|
||||
let currentFileStream = null;
|
||||
let currentWriteCallback = null;
|
||||
let queuedError = null;
|
||||
const onQueuedError = (err) => {
|
||||
queuedError ??= err;
|
||||
if (!writable.destroyed) writable.destroy(err);
|
||||
};
|
||||
const writable = new Writable({
|
||||
async write(chunk, _, cb) {
|
||||
try {
|
||||
unpacker.write(chunk);
|
||||
if (unpacker.isEntryActive()) {
|
||||
if (currentFileStream && currentWriteCallback) {
|
||||
let needsDrain = false;
|
||||
const writeCallback = currentWriteCallback;
|
||||
while (!unpacker.isBodyComplete()) {
|
||||
needsDrain = false;
|
||||
if (unpacker.streamBody(writeCallback) === 0) if (needsDrain) await currentFileStream.waitDrain();
|
||||
else {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (!unpacker.skipPadding()) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
const streamToClose = currentFileStream;
|
||||
if (streamToClose) opQueue.add(() => streamToClose.end()).catch(onQueuedError);
|
||||
currentFileStream = null;
|
||||
currentWriteCallback = null;
|
||||
} else if (!unpacker.skipEntry()) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (true) {
|
||||
const header = unpacker.readHeader();
|
||||
if (header === void 0 || header === null) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
const transformedHeader = transformHeader(header, options);
|
||||
if (!transformedHeader) {
|
||||
if (!unpacker.skipEntry()) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const outPath = await opQueue.add(() => pathCache.preparePath(transformedHeader));
|
||||
if (outPath) {
|
||||
const safeMode = transformedHeader.mode ? transformedHeader.mode & 511 : void 0;
|
||||
const fileStream = createFileSink(outPath, {
|
||||
mode: options.fmode ?? safeMode,
|
||||
mtime: transformedHeader.mtime ?? void 0
|
||||
});
|
||||
let needsDrain = false;
|
||||
const writeCallback = (chunk) => {
|
||||
const writeOk = fileStream.write(chunk);
|
||||
if (!writeOk) needsDrain = true;
|
||||
return writeOk;
|
||||
};
|
||||
while (!unpacker.isBodyComplete()) {
|
||||
needsDrain = false;
|
||||
if (unpacker.streamBody(writeCallback) === 0) if (needsDrain) await fileStream.waitDrain();
|
||||
else {
|
||||
currentFileStream = fileStream;
|
||||
currentWriteCallback = writeCallback;
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (!unpacker.skipPadding()) {
|
||||
currentFileStream = fileStream;
|
||||
currentWriteCallback = writeCallback;
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
opQueue.add(() => fileStream.end()).catch(onQueuedError);
|
||||
} else if (!unpacker.skipEntry()) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
cb(err);
|
||||
}
|
||||
},
|
||||
async final(cb) {
|
||||
try {
|
||||
unpacker.end();
|
||||
unpacker.validateEOF();
|
||||
await pathCache.ready();
|
||||
await opQueue.onIdle();
|
||||
if (queuedError) throw queuedError;
|
||||
await pathCache.applyLinks();
|
||||
cb();
|
||||
} catch (err) {
|
||||
cb(err);
|
||||
}
|
||||
},
|
||||
destroy(error, callback) {
|
||||
(async () => {
|
||||
if (currentFileStream) {
|
||||
currentFileStream.destroy(error ?? void 0);
|
||||
currentFileStream = null;
|
||||
currentWriteCallback = null;
|
||||
}
|
||||
await opQueue.onIdle();
|
||||
})().then(() => callback(error ?? null), (e) => callback(error ?? (e instanceof Error ? e : /* @__PURE__ */ new Error("Stream destroyed"))));
|
||||
}
|
||||
});
|
||||
return writable;
|
||||
}
|
||||
//#endregion
|
||||
export { packTar, packTarSources, unpackTar };
|
||||
77
node_modules/modern-tar/dist/types-D19dF2SE.d.ts
generated
vendored
Normal file
77
node_modules/modern-tar/dist/types-D19dF2SE.d.ts
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
//#region src/tar/constants.d.ts
|
||||
/** Type flag constants for file types. */
|
||||
declare const TYPEFLAG: {
|
||||
readonly file: "0";
|
||||
readonly link: "1";
|
||||
readonly symlink: "2";
|
||||
readonly "character-device": "3";
|
||||
readonly "block-device": "4";
|
||||
readonly directory: "5";
|
||||
readonly fifo: "6";
|
||||
readonly "pax-header": "x";
|
||||
readonly "pax-global-header": "g";
|
||||
readonly "gnu-long-name": "L";
|
||||
readonly "gnu-long-link-name": "K";
|
||||
};
|
||||
//#endregion
|
||||
//#region src/tar/types.d.ts
|
||||
/**
|
||||
* Header information for a tar entry in USTAR format.
|
||||
*/
|
||||
interface TarHeader {
|
||||
/** Entry name/path. Can be up to 255 characters with USTAR prefix extension. */
|
||||
name: string;
|
||||
/** Size of the entry data in bytes. Should be 0 for directories, symlinks, and hardlinks. */
|
||||
size: number;
|
||||
/** Modification time as a `Date` object. Defaults to current time if not specified. */
|
||||
mtime?: Date;
|
||||
/** Unix file permissions as an octal number (e.g., 0o644 for rw-r--r--). Defaults to 0o644 for files and 0o755 for directories. */
|
||||
mode?: number;
|
||||
/** Entry type. Defaults to "file" if not specified. */
|
||||
type?: keyof typeof TYPEFLAG;
|
||||
/** User ID of the entry owner. */
|
||||
uid?: number;
|
||||
/** Group ID of the entry owner. */
|
||||
gid?: number;
|
||||
/** User name of the entry owner. */
|
||||
uname?: string;
|
||||
/** Group name of the entry owner. */
|
||||
gname?: string;
|
||||
/** Target path for symlinks and hard links. */
|
||||
linkname?: string;
|
||||
/** PAX extended attributes as key-value pairs. */
|
||||
pax?: Record<string, string>;
|
||||
}
|
||||
/**
|
||||
* Union type for entry body data that can be packed into a tar archive.
|
||||
*/
|
||||
type TarEntryData = string | Uint8Array | ArrayBuffer | Blob | null | undefined;
|
||||
/**
|
||||
* Configuration options for creating a tar decoder stream.
|
||||
*/
|
||||
interface DecoderOptions {
|
||||
/**
|
||||
* Enable strict validation of the tar archive.
|
||||
* When true, the decoder will throw errors for data corruption issues:
|
||||
* - Invalid checksums (indicates header corruption)
|
||||
* - Invalid USTAR magic string (format violation)
|
||||
* @default false
|
||||
*/
|
||||
strict?: boolean;
|
||||
}
|
||||
/**
|
||||
* Platform-neutral configuration options for extracting tar archives.
|
||||
*
|
||||
* These options work with any tar extraction implementation and are not tied
|
||||
* to specific platforms like Node.js filesystem APIs.
|
||||
*/
|
||||
interface UnpackOptions extends DecoderOptions {
|
||||
/** Number of leading path components to strip from entry names (e.g., strip: 1 removes first directory) */
|
||||
strip?: number;
|
||||
/** Filter function to include/exclude entries (return false to skip) */
|
||||
filter?: (header: TarHeader) => boolean;
|
||||
/** Transform function to modify tar headers before extraction */
|
||||
map?: (header: TarHeader) => TarHeader;
|
||||
}
|
||||
//#endregion
|
||||
export { UnpackOptions as i, TarEntryData as n, TarHeader as r, DecoderOptions as t };
|
||||
636
node_modules/modern-tar/dist/unpacker-CPCEF5CT.js
generated
vendored
Normal file
636
node_modules/modern-tar/dist/unpacker-CPCEF5CT.js
generated
vendored
Normal file
@@ -0,0 +1,636 @@
|
||||
const FILE = "file";
|
||||
const LINK = "link";
|
||||
const SYMLINK = "symlink";
|
||||
const DIRECTORY = "directory";
|
||||
const TYPEFLAG = {
|
||||
file: "0",
|
||||
link: "1",
|
||||
symlink: "2",
|
||||
"character-device": "3",
|
||||
"block-device": "4",
|
||||
directory: "5",
|
||||
fifo: "6",
|
||||
"pax-header": "x",
|
||||
"pax-global-header": "g",
|
||||
"gnu-long-name": "L",
|
||||
"gnu-long-link-name": "K"
|
||||
};
|
||||
const FLAGTYPE = {
|
||||
"0": FILE,
|
||||
"1": LINK,
|
||||
"2": SYMLINK,
|
||||
"3": "character-device",
|
||||
"4": "block-device",
|
||||
"5": DIRECTORY,
|
||||
"6": "fifo",
|
||||
x: "pax-header",
|
||||
g: "pax-global-header",
|
||||
L: "gnu-long-name",
|
||||
K: "gnu-long-link-name"
|
||||
};
|
||||
const ZERO_BLOCK = new Uint8Array(512);
|
||||
const EMPTY = new Uint8Array(0);
|
||||
//#endregion
|
||||
//#region src/tar/encoding.ts
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
function writeString(view, offset, size, value) {
|
||||
if (value) encoder.encodeInto(value, view.subarray(offset, offset + size));
|
||||
}
|
||||
function writeOctal(view, offset, size, value) {
|
||||
if (value === void 0) return;
|
||||
const octalString = value.toString(8).padStart(size - 1, "0");
|
||||
encoder.encodeInto(octalString, view.subarray(offset, offset + size - 1));
|
||||
}
|
||||
function readString(view, offset, size) {
|
||||
const end = view.indexOf(0, offset);
|
||||
const sliceEnd = end === -1 || end > offset + size ? offset + size : end;
|
||||
return decoder.decode(view.subarray(offset, sliceEnd));
|
||||
}
|
||||
function readOctal(view, offset, size) {
|
||||
let value = 0;
|
||||
const end = offset + size;
|
||||
for (let i = offset; i < end; i++) {
|
||||
const charCode = view[i];
|
||||
if (charCode === 0) break;
|
||||
if (charCode === 32) continue;
|
||||
value = value * 8 + (charCode - 48);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function readNumeric(view, offset, size) {
|
||||
if (view[offset] & 128) {
|
||||
let result = 0;
|
||||
result = view[offset] & 127;
|
||||
for (let i = 1; i < size; i++) result = result * 256 + view[offset + i];
|
||||
if (!Number.isSafeInteger(result)) throw new Error("TAR number too large");
|
||||
return result;
|
||||
}
|
||||
return readOctal(view, offset, size);
|
||||
}
|
||||
//#endregion
|
||||
//#region src/tar/body.ts
|
||||
const isBodyless = (header) => header.type === "directory" || header.type === "symlink" || header.type === "link" || header.type === "character-device" || header.type === "block-device" || header.type === "fifo";
|
||||
async function normalizeBody(body) {
|
||||
if (body === null || body === void 0) return EMPTY;
|
||||
if (body instanceof Uint8Array) return body;
|
||||
if (typeof body === "string") return encoder.encode(body);
|
||||
if (body instanceof ArrayBuffer) return new Uint8Array(body);
|
||||
if (body instanceof Blob) return new Uint8Array(await body.arrayBuffer());
|
||||
throw new TypeError("Unsupported content type for entry body.");
|
||||
}
|
||||
//#endregion
|
||||
//#region src/tar/options.ts
|
||||
const stripPath = (p, n) => {
|
||||
const parts = p.split("/").filter(Boolean);
|
||||
return n >= parts.length ? "" : parts.slice(n).join("/");
|
||||
};
|
||||
function transformHeader(header, options) {
|
||||
const { strip, filter, map } = options;
|
||||
if (!strip && !filter && !map) return header;
|
||||
const h = { ...header };
|
||||
if (strip && strip > 0) {
|
||||
const newName = stripPath(h.name, strip);
|
||||
if (!newName) return null;
|
||||
h.name = h.type === "directory" && !newName.endsWith("/") ? `${newName}/` : newName;
|
||||
if (h.linkname) {
|
||||
const isAbsolute = h.linkname.startsWith("/");
|
||||
if (isAbsolute || h.type === "link") {
|
||||
const stripped = stripPath(h.linkname, strip);
|
||||
h.linkname = isAbsolute ? `/${stripped}` || "/" : stripped;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filter?.(h) === false) return null;
|
||||
const result = map ? map(h) : h;
|
||||
if (result && (!result.name || !result.name.trim() || result.name === "." || result.name === "/")) return null;
|
||||
return result;
|
||||
}
|
||||
//#endregion
|
||||
//#region src/tar/checksum.ts
|
||||
const CHECKSUM_SPACE = 32;
|
||||
const ASCII_ZERO = 48;
|
||||
function validateChecksum(block) {
|
||||
const stored = readOctal(block, 148, 8);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < block.length; i++) if (i >= 148 && i < 156) sum += CHECKSUM_SPACE;
|
||||
else sum += block[i];
|
||||
return stored === sum;
|
||||
}
|
||||
function writeChecksum(block) {
|
||||
block.fill(CHECKSUM_SPACE, 148, 156);
|
||||
let checksum = 0;
|
||||
for (const byte of block) checksum += byte;
|
||||
for (let i = 153; i >= 148; i--) {
|
||||
block[i] = (checksum & 7) + ASCII_ZERO;
|
||||
checksum >>= 3;
|
||||
}
|
||||
block[154] = 0;
|
||||
block[155] = CHECKSUM_SPACE;
|
||||
}
|
||||
//#endregion
|
||||
//#region src/tar/pax.ts
|
||||
const USTAR_SPLIT_MAX_SIZE = 256;
|
||||
function generatePax(header) {
|
||||
const paxRecords = {};
|
||||
if (encoder.encode(header.name).length > 100) {
|
||||
if (findUstarSplit(header.name) === null) paxRecords.path = header.name;
|
||||
}
|
||||
if (header.linkname && encoder.encode(header.linkname).length > 100) paxRecords.linkpath = header.linkname;
|
||||
if (header.uname && encoder.encode(header.uname).length > 32) paxRecords.uname = header.uname;
|
||||
if (header.gname && encoder.encode(header.gname).length > 32) paxRecords.gname = header.gname;
|
||||
if (header.uid != null && header.uid > 2097151) paxRecords.uid = String(header.uid);
|
||||
if (header.gid != null && header.gid > 2097151) paxRecords.gid = String(header.gid);
|
||||
if (header.size != null && header.size > 8589934591) paxRecords.size = String(header.size);
|
||||
if (header.pax) Object.assign(paxRecords, header.pax);
|
||||
const paxEntries = Object.entries(paxRecords);
|
||||
if (paxEntries.length === 0) return null;
|
||||
const paxBody = encoder.encode(paxEntries.map(([key, value]) => {
|
||||
const record = `${key}=${value}\n`;
|
||||
const partLength = encoder.encode(record).length + 1;
|
||||
let totalLength = partLength + String(partLength).length;
|
||||
totalLength = partLength + String(totalLength).length;
|
||||
return `${totalLength} ${record}`;
|
||||
}).join(""));
|
||||
return {
|
||||
paxHeader: createTarHeader({
|
||||
name: decoder.decode(encoder.encode(`PaxHeader/${header.name}`).slice(0, 100)),
|
||||
size: paxBody.length,
|
||||
type: "pax-header",
|
||||
mode: 420,
|
||||
mtime: header.mtime,
|
||||
uname: header.uname,
|
||||
gname: header.gname,
|
||||
uid: header.uid,
|
||||
gid: header.gid
|
||||
}),
|
||||
paxBody
|
||||
};
|
||||
}
|
||||
function findUstarSplit(path) {
|
||||
const totalPathBytes = encoder.encode(path).length;
|
||||
if (totalPathBytes <= 100 || totalPathBytes > USTAR_SPLIT_MAX_SIZE) return null;
|
||||
for (let i = path.length - 1; i > 0; i--) {
|
||||
if (path[i] !== "/") continue;
|
||||
const prefix = path.slice(0, i);
|
||||
const name = path.slice(i + 1);
|
||||
if (encoder.encode(prefix).length <= 155 && encoder.encode(name).length <= 100) return {
|
||||
prefix,
|
||||
name
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//#endregion
|
||||
//#region src/tar/header.ts
|
||||
function createTarHeader(header) {
|
||||
const view = new Uint8Array(512);
|
||||
const size = isBodyless(header) ? 0 : header.size ?? 0;
|
||||
let name = header.name;
|
||||
let prefix = "";
|
||||
if (!header.pax?.path) {
|
||||
const split = findUstarSplit(name);
|
||||
if (split) {
|
||||
name = split.name;
|
||||
prefix = split.prefix;
|
||||
}
|
||||
}
|
||||
writeString(view, 0, 100, name);
|
||||
writeOctal(view, 100, 8, header.mode ?? (header.type === "directory" ? 493 : 420));
|
||||
writeOctal(view, 108, 8, header.uid ?? 0);
|
||||
writeOctal(view, 116, 8, header.gid ?? 0);
|
||||
writeOctal(view, 124, 12, size);
|
||||
writeOctal(view, 136, 12, Math.floor((header.mtime?.getTime() ?? Date.now()) / 1e3));
|
||||
writeString(view, 156, 1, TYPEFLAG[header.type ?? "file"]);
|
||||
writeString(view, 157, 100, header.linkname);
|
||||
writeString(view, 257, 6, "ustar\0");
|
||||
writeString(view, 263, 2, "00");
|
||||
writeString(view, 265, 32, header.uname);
|
||||
writeString(view, 297, 32, header.gname);
|
||||
writeString(view, 345, 155, prefix);
|
||||
writeChecksum(view);
|
||||
return view;
|
||||
}
|
||||
function parseUstarHeader(block, strict) {
|
||||
if (strict && !validateChecksum(block)) throw new Error("Invalid tar header checksum.");
|
||||
const typeflag = readString(block, 156, 1);
|
||||
const header = {
|
||||
name: readString(block, 0, 100),
|
||||
mode: readOctal(block, 100, 8),
|
||||
uid: readNumeric(block, 108, 8),
|
||||
gid: readNumeric(block, 116, 8),
|
||||
size: readNumeric(block, 124, 12),
|
||||
mtime: /* @__PURE__ */ new Date(readNumeric(block, 136, 12) * 1e3),
|
||||
type: FLAGTYPE[typeflag] || "file",
|
||||
linkname: readString(block, 157, 100)
|
||||
};
|
||||
const magic = readString(block, 257, 6);
|
||||
if (isBodyless(header)) header.size = 0;
|
||||
if (magic.trim() === "ustar") {
|
||||
header.uname = readString(block, 265, 32);
|
||||
header.gname = readString(block, 297, 32);
|
||||
}
|
||||
if (magic === "ustar") header.prefix = readString(block, 345, 155);
|
||||
return header;
|
||||
}
|
||||
const PAX_MAPPING = {
|
||||
path: ["name", (v) => v],
|
||||
linkpath: ["linkname", (v) => v],
|
||||
size: ["size", (v) => parseInt(v, 10)],
|
||||
mtime: ["mtime", parseFloat],
|
||||
uid: ["uid", (v) => parseInt(v, 10)],
|
||||
gid: ["gid", (v) => parseInt(v, 10)],
|
||||
uname: ["uname", (v) => v],
|
||||
gname: ["gname", (v) => v]
|
||||
};
|
||||
function parsePax(buffer) {
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
const overrides = Object.create(null);
|
||||
const pax = Object.create(null);
|
||||
let offset = 0;
|
||||
while (offset < buffer.length) {
|
||||
const spaceIndex = buffer.indexOf(32, offset);
|
||||
if (spaceIndex === -1) break;
|
||||
const length = parseInt(decoder.decode(buffer.subarray(offset, spaceIndex)), 10);
|
||||
if (Number.isNaN(length) || length === 0) break;
|
||||
const recordEnd = offset + length;
|
||||
const [key, value] = decoder.decode(buffer.subarray(spaceIndex + 1, recordEnd - 1)).split("=", 2);
|
||||
if (key && value !== void 0) {
|
||||
pax[key] = value;
|
||||
if (Object.hasOwn(PAX_MAPPING, key)) {
|
||||
const [targetKey, parser] = PAX_MAPPING[key];
|
||||
const parsedValue = parser(value);
|
||||
if (typeof parsedValue === "string" || !Number.isNaN(parsedValue)) overrides[targetKey] = parsedValue;
|
||||
}
|
||||
}
|
||||
offset = recordEnd;
|
||||
}
|
||||
if (Object.keys(pax).length > 0) overrides.pax = pax;
|
||||
return overrides;
|
||||
}
|
||||
function applyOverrides(header, overrides) {
|
||||
if (overrides.name !== void 0) header.name = overrides.name;
|
||||
if (overrides.linkname !== void 0) header.linkname = overrides.linkname;
|
||||
if (overrides.size !== void 0) header.size = overrides.size;
|
||||
if (overrides.mtime !== void 0) header.mtime = /* @__PURE__ */ new Date(overrides.mtime * 1e3);
|
||||
if (overrides.uid !== void 0) header.uid = overrides.uid;
|
||||
if (overrides.gid !== void 0) header.gid = overrides.gid;
|
||||
if (overrides.uname !== void 0) header.uname = overrides.uname;
|
||||
if (overrides.gname !== void 0) header.gname = overrides.gname;
|
||||
if (overrides.pax) header.pax = Object.assign({}, header.pax ?? {}, overrides.pax);
|
||||
}
|
||||
function getMetaParser(type) {
|
||||
switch (type) {
|
||||
case "pax-global-header":
|
||||
case "pax-header": return parsePax;
|
||||
case "gnu-long-name": return (data) => ({ name: readString(data, 0, data.length) });
|
||||
case "gnu-long-link-name": return (data) => ({ linkname: readString(data, 0, data.length) });
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
function getHeaderBlocks(header) {
|
||||
const base = createTarHeader(header);
|
||||
const pax = generatePax(header);
|
||||
if (!pax) return [base];
|
||||
const paxPadding = -pax.paxBody.length & 511;
|
||||
const paddingBlocks = paxPadding > 0 ? [ZERO_BLOCK.subarray(0, paxPadding)] : [];
|
||||
return [
|
||||
pax.paxHeader,
|
||||
pax.paxBody,
|
||||
...paddingBlocks,
|
||||
base
|
||||
];
|
||||
}
|
||||
//#endregion
|
||||
//#region src/tar/packer.ts
|
||||
const EOF_BUFFER = new Uint8Array(512 * 2);
|
||||
function createTarPacker(onData, onError, onFinalize) {
|
||||
let currentHeader = null;
|
||||
let bytesWritten = 0;
|
||||
let finalized = false;
|
||||
const fail = (message) => {
|
||||
const error = new Error(message);
|
||||
onError(error);
|
||||
throw error;
|
||||
};
|
||||
return {
|
||||
add(header) {
|
||||
if (finalized) fail("No new tar entries after finalize.");
|
||||
if (currentHeader !== null) fail("Previous entry must be completed before adding a new one");
|
||||
const size = isBodyless(header) ? 0 : header.size;
|
||||
if (!Number.isSafeInteger(size) || size < 0) fail("Invalid tar entry size.");
|
||||
try {
|
||||
const headerBlocks = getHeaderBlocks({
|
||||
...header,
|
||||
size
|
||||
});
|
||||
for (const block of headerBlocks) onData(block);
|
||||
currentHeader = {
|
||||
...header,
|
||||
size
|
||||
};
|
||||
bytesWritten = 0;
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
},
|
||||
write(chunk) {
|
||||
if (!currentHeader) fail("No active tar entry.");
|
||||
if (finalized) fail("Cannot write data after finalize.");
|
||||
const newTotal = bytesWritten + chunk.length;
|
||||
if (newTotal > currentHeader.size) fail(`"${currentHeader.name}" exceeds given size of ${currentHeader.size} bytes.`);
|
||||
try {
|
||||
bytesWritten = newTotal;
|
||||
onData(chunk);
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
},
|
||||
endEntry() {
|
||||
if (!currentHeader) fail("No active entry to end.");
|
||||
if (finalized) fail("Cannot end entry after finalize.");
|
||||
try {
|
||||
if (bytesWritten !== currentHeader.size) fail(`Size mismatch for "${currentHeader.name}".`);
|
||||
const paddingSize = -currentHeader.size & 511;
|
||||
if (paddingSize > 0) onData(new Uint8Array(paddingSize));
|
||||
currentHeader = null;
|
||||
bytesWritten = 0;
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
finalize() {
|
||||
if (finalized) fail("Archive has already been finalized");
|
||||
if (currentHeader !== null) fail("Cannot finalize while an entry is still active");
|
||||
try {
|
||||
onData(EOF_BUFFER);
|
||||
finalized = true;
|
||||
if (onFinalize) onFinalize();
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
//#region src/tar/chunk-queue.ts
|
||||
const INITIAL_CAPACITY = 256;
|
||||
function createChunkQueue() {
|
||||
let chunks = new Array(INITIAL_CAPACITY);
|
||||
let capacityMask = chunks.length - 1;
|
||||
let head = 0;
|
||||
let tail = 0;
|
||||
let totalAvailable = 0;
|
||||
const consumeFromHead = (count) => {
|
||||
const chunk = chunks[head];
|
||||
if (count === chunk.length) {
|
||||
chunks[head] = EMPTY;
|
||||
head = head + 1 & capacityMask;
|
||||
} else chunks[head] = chunk.subarray(count);
|
||||
totalAvailable -= count;
|
||||
if (totalAvailable === 0 && chunks.length > INITIAL_CAPACITY) {
|
||||
chunks = new Array(INITIAL_CAPACITY);
|
||||
capacityMask = INITIAL_CAPACITY - 1;
|
||||
head = 0;
|
||||
tail = 0;
|
||||
}
|
||||
};
|
||||
function pull(bytes, callback) {
|
||||
if (callback) {
|
||||
let fed = 0;
|
||||
let remaining = Math.min(bytes, totalAvailable);
|
||||
while (remaining > 0) {
|
||||
const chunk = chunks[head];
|
||||
const toFeed = Math.min(remaining, chunk.length);
|
||||
const segment = toFeed === chunk.length ? chunk : chunk.subarray(0, toFeed);
|
||||
consumeFromHead(toFeed);
|
||||
remaining -= toFeed;
|
||||
fed += toFeed;
|
||||
if (!callback(segment)) break;
|
||||
}
|
||||
return fed;
|
||||
}
|
||||
if (totalAvailable < bytes) return null;
|
||||
if (bytes === 0) return EMPTY;
|
||||
const firstChunk = chunks[head];
|
||||
if (firstChunk.length >= bytes) {
|
||||
const view = firstChunk.length === bytes ? firstChunk : firstChunk.subarray(0, bytes);
|
||||
consumeFromHead(bytes);
|
||||
return view;
|
||||
}
|
||||
const result = new Uint8Array(bytes);
|
||||
let copied = 0;
|
||||
let remaining = bytes;
|
||||
while (remaining > 0) {
|
||||
const chunk = chunks[head];
|
||||
const toCopy = Math.min(remaining, chunk.length);
|
||||
result.set(toCopy === chunk.length ? chunk : chunk.subarray(0, toCopy), copied);
|
||||
copied += toCopy;
|
||||
remaining -= toCopy;
|
||||
consumeFromHead(toCopy);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
push: (chunk) => {
|
||||
if (chunk.length === 0) return;
|
||||
let nextTail = tail + 1 & capacityMask;
|
||||
if (nextTail === head) {
|
||||
const oldLen = chunks.length;
|
||||
const newLen = oldLen * 2;
|
||||
const newChunks = new Array(newLen);
|
||||
const count = tail - head + oldLen & oldLen - 1;
|
||||
if (head < tail) for (let i = 0; i < count; i++) newChunks[i] = chunks[head + i];
|
||||
else if (count > 0) {
|
||||
const firstPart = oldLen - head;
|
||||
for (let i = 0; i < firstPart; i++) newChunks[i] = chunks[head + i];
|
||||
for (let i = 0; i < tail; i++) newChunks[firstPart + i] = chunks[i];
|
||||
}
|
||||
chunks = newChunks;
|
||||
capacityMask = newLen - 1;
|
||||
head = 0;
|
||||
tail = count;
|
||||
nextTail = tail + 1 & capacityMask;
|
||||
}
|
||||
chunks[tail] = chunk;
|
||||
tail = nextTail;
|
||||
totalAvailable += chunk.length;
|
||||
},
|
||||
available: () => totalAvailable,
|
||||
peek: (bytes) => {
|
||||
if (totalAvailable < bytes) return null;
|
||||
if (bytes === 0) return EMPTY;
|
||||
const firstChunk = chunks[head];
|
||||
if (firstChunk.length >= bytes) return firstChunk.length === bytes ? firstChunk : firstChunk.subarray(0, bytes);
|
||||
const result = new Uint8Array(bytes);
|
||||
let copied = 0;
|
||||
let index = head;
|
||||
while (copied < bytes) {
|
||||
const chunk = chunks[index];
|
||||
const toCopy = Math.min(bytes - copied, chunk.length);
|
||||
if (toCopy === chunk.length) result.set(chunk, copied);
|
||||
else result.set(chunk.subarray(0, toCopy), copied);
|
||||
copied += toCopy;
|
||||
index = index + 1 & capacityMask;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
discard: (bytes) => {
|
||||
if (bytes > totalAvailable) throw new Error("Too many bytes consumed");
|
||||
if (bytes === 0) return;
|
||||
let remaining = bytes;
|
||||
while (remaining > 0) {
|
||||
const chunk = chunks[head];
|
||||
const toConsume = Math.min(remaining, chunk.length);
|
||||
consumeFromHead(toConsume);
|
||||
remaining -= toConsume;
|
||||
}
|
||||
},
|
||||
pull
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
//#region src/tar/unpacker.ts
|
||||
const STATE_HEADER = 0;
|
||||
const STATE_BODY = 1;
|
||||
const truncateErr = /* @__PURE__ */ new Error("Tar archive is truncated.");
|
||||
function createUnpacker(options = {}) {
|
||||
const strict = options.strict ?? false;
|
||||
const { available, peek, push, discard, pull } = createChunkQueue();
|
||||
let state = STATE_HEADER;
|
||||
let ended = false;
|
||||
let done = false;
|
||||
let eof = false;
|
||||
let currentEntry = null;
|
||||
const paxGlobals = {};
|
||||
let nextEntryOverrides = {};
|
||||
const unpacker = {
|
||||
isEntryActive: () => state === STATE_BODY,
|
||||
isBodyComplete: () => !currentEntry || currentEntry.remaining === 0,
|
||||
canFinish: () => !currentEntry || available() >= currentEntry.remaining + currentEntry.padding,
|
||||
write(chunk) {
|
||||
if (ended) throw new Error("Archive already ended.");
|
||||
push(chunk);
|
||||
},
|
||||
end() {
|
||||
ended = true;
|
||||
},
|
||||
readHeader() {
|
||||
if (state !== STATE_HEADER) throw new Error("Cannot read header while an entry is active");
|
||||
if (done) return void 0;
|
||||
while (!done) {
|
||||
if (available() < 512) {
|
||||
if (ended) {
|
||||
if (available() > 0 && strict) throw truncateErr;
|
||||
done = true;
|
||||
return;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const headerBlock = peek(512);
|
||||
if (isZeroBlock(headerBlock)) {
|
||||
if (available() < 512 * 2) {
|
||||
if (ended) {
|
||||
if (strict) throw truncateErr;
|
||||
done = true;
|
||||
return;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (isZeroBlock(peek(512 * 2).subarray(512))) {
|
||||
discard(512 * 2);
|
||||
done = true;
|
||||
eof = true;
|
||||
return;
|
||||
}
|
||||
if (strict) throw new Error("Invalid tar header.");
|
||||
discard(512);
|
||||
continue;
|
||||
}
|
||||
let internalHeader;
|
||||
try {
|
||||
internalHeader = parseUstarHeader(headerBlock, strict);
|
||||
} catch (err) {
|
||||
if (strict) throw err;
|
||||
discard(512);
|
||||
continue;
|
||||
}
|
||||
const metaParser = getMetaParser(internalHeader.type);
|
||||
if (metaParser) {
|
||||
const paddedSize = internalHeader.size + (-internalHeader.size & 511);
|
||||
if (available() < 512 + paddedSize) {
|
||||
if (ended && strict) throw truncateErr;
|
||||
return null;
|
||||
}
|
||||
discard(512);
|
||||
const overrides = metaParser(pull(paddedSize).subarray(0, internalHeader.size));
|
||||
const target = internalHeader.type === "pax-global-header" ? paxGlobals : nextEntryOverrides;
|
||||
for (const key in overrides) target[key] = overrides[key];
|
||||
continue;
|
||||
}
|
||||
discard(512);
|
||||
const header = internalHeader;
|
||||
if (internalHeader.prefix) header.name = `${internalHeader.prefix}/${header.name}`;
|
||||
applyOverrides(header, paxGlobals);
|
||||
applyOverrides(header, nextEntryOverrides);
|
||||
if (header.name.endsWith("/") && header.type === "file") header.type = DIRECTORY;
|
||||
nextEntryOverrides = {};
|
||||
currentEntry = {
|
||||
header,
|
||||
remaining: header.size,
|
||||
padding: -header.size & 511
|
||||
};
|
||||
state = STATE_BODY;
|
||||
return header;
|
||||
}
|
||||
},
|
||||
streamBody(callback) {
|
||||
if (state !== STATE_BODY || !currentEntry || currentEntry.remaining === 0) return 0;
|
||||
const bytesToFeed = Math.min(currentEntry.remaining, available());
|
||||
if (bytesToFeed === 0) return 0;
|
||||
const fed = pull(bytesToFeed, callback);
|
||||
currentEntry.remaining -= fed;
|
||||
return fed;
|
||||
},
|
||||
skipPadding() {
|
||||
if (state !== STATE_BODY || !currentEntry) return true;
|
||||
if (currentEntry.remaining > 0) throw new Error("Body not fully consumed");
|
||||
if (available() < currentEntry.padding) return false;
|
||||
discard(currentEntry.padding);
|
||||
currentEntry = null;
|
||||
state = STATE_HEADER;
|
||||
return true;
|
||||
},
|
||||
skipEntry() {
|
||||
if (state !== STATE_BODY || !currentEntry) return true;
|
||||
const toDiscard = Math.min(currentEntry.remaining, available());
|
||||
if (toDiscard > 0) {
|
||||
discard(toDiscard);
|
||||
currentEntry.remaining -= toDiscard;
|
||||
}
|
||||
if (currentEntry.remaining > 0) return false;
|
||||
return unpacker.skipPadding();
|
||||
},
|
||||
validateEOF() {
|
||||
if (strict) {
|
||||
if (!eof) throw truncateErr;
|
||||
if (available() > 0) {
|
||||
if (pull(available()).some((byte) => byte !== 0)) throw new Error("Invalid EOF.");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return unpacker;
|
||||
}
|
||||
function isZeroBlock(block) {
|
||||
if (block.byteOffset % 8 === 0) {
|
||||
const view = new BigUint64Array(block.buffer, block.byteOffset, block.length / 8);
|
||||
for (let i = 0; i < view.length; i++) if (view[i] !== 0n) return false;
|
||||
return true;
|
||||
}
|
||||
for (let i = 0; i < block.length; i++) if (block[i] !== 0) return false;
|
||||
return true;
|
||||
}
|
||||
//#endregion
|
||||
export { normalizeBody as a, LINK as c, isBodyless as i, SYMLINK as l, createTarPacker as n, DIRECTORY as o, transformHeader as r, FILE as s, createUnpacker as t };
|
||||
329
node_modules/modern-tar/dist/web/index.d.ts
generated
vendored
Normal file
329
node_modules/modern-tar/dist/web/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,329 @@
|
||||
import { i as UnpackOptions, n as TarEntryData, r as TarHeader, t as DecoderOptions } from "../types-D19dF2SE.js";
|
||||
|
||||
//#region src/web/compression.d.ts
|
||||
/**
|
||||
* Creates a gzip compression stream that is compatible with Uint8Array streams.
|
||||
*
|
||||
* @returns A {@link ReadableWritablePair} configured for gzip compression.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createGzipEncoder, createTarPacker } from 'modern-tar';
|
||||
*
|
||||
* // Create and compress a tar archive
|
||||
* const { readable, controller } = createTarPacker();
|
||||
* const compressedStream = readable.pipeThrough(createGzipEncoder());
|
||||
*
|
||||
* // Add entries...
|
||||
* const fileStream = controller.add({ name: "file.txt", size: 5, type: "file" });
|
||||
* const writer = fileStream.getWriter();
|
||||
* await writer.write(new TextEncoder().encode("hello"));
|
||||
* await writer.close();
|
||||
* controller.finalize();
|
||||
*
|
||||
* // Upload compressed .tar.gz
|
||||
* await fetch('/api/upload', {
|
||||
* method: 'POST',
|
||||
* body: compressedStream,
|
||||
* headers: { 'Content-Type': 'application/gzip' }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
declare function createGzipEncoder(): ReadableWritablePair<Uint8Array, Uint8Array>;
|
||||
/**
|
||||
* Creates a gzip decompression stream that is compatible with Uint8Array streams.
|
||||
*
|
||||
* @returns A {@link ReadableWritablePair} configured for gzip decompression.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createGzipDecoder, createTarDecoder } from 'modern-tar';
|
||||
*
|
||||
* // Download and process a .tar.gz file
|
||||
* const response = await fetch('https://api.example.com/archive.tar.gz');
|
||||
* if (!response.body) throw new Error('No response body');
|
||||
*
|
||||
* // Buffer entire archive
|
||||
* const entries = await unpackTar(response.body.pipeThrough(createGzipDecoder()));
|
||||
*
|
||||
* for (const entry of entries) {
|
||||
* console.log(`Extracted: ${entry.header.name}`);
|
||||
* const content = new TextDecoder().decode(entry.data);
|
||||
* console.log(`Content: ${content}`);
|
||||
* }
|
||||
* ```
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createGzipDecoder, createTarDecoder } from 'modern-tar';
|
||||
*
|
||||
* // Download and process a .tar.gz file
|
||||
* const response = await fetch('https://api.example.com/archive.tar.gz');
|
||||
* if (!response.body) throw new Error('No response body');
|
||||
*
|
||||
* // Chain decompression and tar parsing using streams
|
||||
* const entries = response.body
|
||||
* .pipeThrough(createGzipDecoder())
|
||||
* .pipeThrough(createTarDecoder());
|
||||
*
|
||||
* for await (const entry of entries) {
|
||||
* console.log(`Extracted: ${entry.header.name}`);
|
||||
* // Process entry.body ReadableStream as needed
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
declare function createGzipDecoder(): ReadableWritablePair<Uint8Array, Uint8Array>;
|
||||
//#endregion
|
||||
//#region src/web/types.d.ts
|
||||
/**
|
||||
* Represents a complete entry to be packed into a tar archive.
|
||||
*
|
||||
* Combines header metadata with optional body data. Used as input to {@link packTar}
|
||||
* and the controller returned by {@link createTarPacker}.
|
||||
*/
|
||||
interface TarEntry {
|
||||
header: TarHeader;
|
||||
body?: TarEntryData | ReadableStream<Uint8Array>;
|
||||
}
|
||||
/**
|
||||
* Represents an entry parsed from a tar archive stream.
|
||||
*/
|
||||
interface ParsedTarEntry {
|
||||
header: TarHeader;
|
||||
body: ReadableStream<Uint8Array>;
|
||||
}
|
||||
/**
|
||||
* Represents an extracted entry with fully buffered content.
|
||||
*
|
||||
* For bodyless entries (directories, symlinks, hardlinks), `data` will be `undefined`.
|
||||
* For files (including empty files), `data` will be a `Uint8Array`.
|
||||
*/
|
||||
interface ParsedTarEntryWithData {
|
||||
header: TarHeader;
|
||||
data?: Uint8Array;
|
||||
}
|
||||
//#endregion
|
||||
//#region src/web/helpers.d.ts
|
||||
/**
|
||||
* Packs an array of tar entries into a single `Uint8Array` buffer.
|
||||
*
|
||||
* For streaming scenarios or large archives, use {@link createTarPacker} instead.
|
||||
*
|
||||
* @param entries - Array of tar entries with headers and optional bodies
|
||||
* @returns A `Promise` that resolves to the complete tar archive as a Uint8Array
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { packTar } from 'modern-tar';
|
||||
*
|
||||
* const entries = [
|
||||
* {
|
||||
* header: { name: "hello.txt", size: 5, type: "file" },
|
||||
* body: "hello"
|
||||
* },
|
||||
* {
|
||||
* header: { name: "data.json", size: 13, type: "file" },
|
||||
* body: new Uint8Array([123, 34, 116, 101, 115, 116, 34, 58, 116, 114, 117, 101, 125]) // {"test":true}
|
||||
* },
|
||||
* {
|
||||
* header: { name: "folder/", type: "directory", size: 0 }
|
||||
* }
|
||||
* ];
|
||||
*
|
||||
* const tarBuffer = await packTar(entries);
|
||||
*
|
||||
* // Save to file or upload
|
||||
* await fetch('/api/upload', {
|
||||
* method: 'POST',
|
||||
* body: tarBuffer,
|
||||
* headers: { 'Content-Type': 'application/x-tar' }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
declare function packTar(entries: (TarEntry | ParsedTarEntryWithData)[]): Promise<Uint8Array>;
|
||||
/**
|
||||
* Extracts all entries and their data from a complete tar archive buffer.
|
||||
*
|
||||
* For streaming scenarios or large archives, use {@link createTarDecoder} instead.
|
||||
*
|
||||
* @param archive - The complete tar archive as `ArrayBuffer` or `Uint8Array`
|
||||
* @param options - Optional extraction configuration
|
||||
* @returns A `Promise` that resolves to an array of entries with buffered data
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { unpackTar } from 'modern-tar';
|
||||
*
|
||||
* // From a file upload or fetch
|
||||
* const response = await fetch('/api/archive.tar');
|
||||
* const tarBuffer = await response.arrayBuffer();
|
||||
*
|
||||
* const entries = await unpackTar(tarBuffer);
|
||||
* for (const entry of entries) {
|
||||
* if (entry.data) {
|
||||
* console.log(`File: ${entry.header.name}, Size: ${entry.data.length} bytes`);
|
||||
* const content = new TextDecoder().decode(entry.data);
|
||||
* console.log(`Content: ${content}`);
|
||||
* } else {
|
||||
* console.log(`${entry.header.type}: ${entry.header.name}`);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
* @example
|
||||
* ```typescript
|
||||
* // From a Uint8Array with options
|
||||
* const tarData = new Uint8Array([...]); // your tar data
|
||||
* const entries = await unpackTar(tarData, {
|
||||
* strip: 1,
|
||||
* filter: (header) => header.name.endsWith('.txt'),
|
||||
* map: (header) => ({ ...header, name: header.name.toLowerCase() })
|
||||
* });
|
||||
*
|
||||
* // Process filtered files
|
||||
* for (const file of entries) {
|
||||
* if (file.data) {
|
||||
* console.log(new TextDecoder().decode(file.data));
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
declare function unpackTar(archive: ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>, options?: UnpackOptions): Promise<ParsedTarEntryWithData[]>;
|
||||
//#endregion
|
||||
//#region src/web/pack.d.ts
|
||||
/**
|
||||
* Controls a streaming tar packing process.
|
||||
*
|
||||
* Provides methods to add entries to a tar archive and finalize the stream.
|
||||
* This is the advanced API for streaming tar creation, allowing you to dynamically
|
||||
* add entries and write their content as a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
|
||||
*/
|
||||
interface TarPackController {
|
||||
/**
|
||||
* Add an entry to the tar archive.
|
||||
*
|
||||
* After adding the entry, you must write exactly `header.size` bytes of data
|
||||
* to the returned [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream)
|
||||
* and then close it. For entries that do not have a body (e.g., directories),
|
||||
* the size property should be set to 0 and the stream should be closed immediately.
|
||||
*
|
||||
* @param header - The tar header for the entry. The `size` property must be accurate
|
||||
* @returns A [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream) for writing the entry's body data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Add a text file
|
||||
* const fileStream = controller.add({
|
||||
* name: "file.txt",
|
||||
* size: 11,
|
||||
* type: "file"
|
||||
* });
|
||||
*
|
||||
* const writer = fileStream.getWriter();
|
||||
* await writer.write(new TextEncoder().encode("hello world"));
|
||||
* await writer.close();
|
||||
*
|
||||
* // Add a directory
|
||||
* const dirStream = controller.add({
|
||||
* name: "folder/",
|
||||
* type: "directory",
|
||||
* size: 0
|
||||
* });
|
||||
* await dirStream.close(); // Directories have no content
|
||||
* ```
|
||||
*/
|
||||
add(header: TarHeader): WritableStream<Uint8Array>;
|
||||
/**
|
||||
* Finalize the archive.
|
||||
*
|
||||
* Must be called after all entries have been added.
|
||||
* This writes the end-of-archive marker and closes the readable stream.
|
||||
*/
|
||||
finalize(): void;
|
||||
/**
|
||||
* Abort the packing process with an error.
|
||||
*
|
||||
* @param err - The error that caused the abort
|
||||
*/
|
||||
error(err: unknown): void;
|
||||
}
|
||||
/**
|
||||
* Create a streaming tar packer.
|
||||
*
|
||||
* Provides a controller-based API for creating tar archives, suitable for scenarios where entries are
|
||||
* generated dynamically. The returned [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
|
||||
* outputs tar archive bytes as entries are added.
|
||||
*
|
||||
* @returns Object containing the readable stream and controller
|
||||
* @returns readable - [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) that outputs the tar archive bytes
|
||||
* @returns controller - {@link TarPackController} for adding entries and finalizing
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createTarPacker } from 'modern-tar';
|
||||
*
|
||||
* const { readable, controller } = createTarPacker();
|
||||
*
|
||||
* // Add entries dynamically
|
||||
* const fileStream = controller.add({
|
||||
* name: "dynamic.txt",
|
||||
* size: 5,
|
||||
* type: "file"
|
||||
* });
|
||||
*
|
||||
* const writer = fileStream.getWriter();
|
||||
* await writer.write(new TextEncoder().encode("hello"));
|
||||
* await writer.close();
|
||||
*
|
||||
* // Add multiple entries
|
||||
* const jsonStream = controller.add({
|
||||
* name: "data.json",
|
||||
* size: 13,
|
||||
* type: "file"
|
||||
* });
|
||||
* const jsonWriter = jsonStream.getWriter();
|
||||
* await jsonWriter.write(new TextEncoder().encode('{"test":true}'));
|
||||
* await jsonWriter.close();
|
||||
*
|
||||
* // Finalize the archive
|
||||
* controller.finalize();
|
||||
*
|
||||
* // Use the readable stream
|
||||
* const response = new Response(readable);
|
||||
* const buffer = await response.arrayBuffer();
|
||||
* ```
|
||||
*/
|
||||
declare function createTarPacker(): {
|
||||
readable: ReadableStream<Uint8Array>;
|
||||
controller: TarPackController;
|
||||
};
|
||||
//#endregion
|
||||
//#region src/web/unpack.d.ts
|
||||
/**
|
||||
* Create a transform stream that parses tar bytes into entries.
|
||||
*
|
||||
* @param options - Optional configuration for the decoder using {@link DecoderOptions}.
|
||||
* @returns `TransformStream` that converts tar archive bytes to {@link ParsedTarEntry} objects.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createTarDecoder } from 'modern-tar';
|
||||
*
|
||||
* const decoder = createTarDecoder({ strict: true });
|
||||
* const entriesStream = tarStream.pipeThrough(decoder);
|
||||
*
|
||||
* for await (const entry of entriesStream) {
|
||||
* console.log(`Entry: ${entry.header.name}`);
|
||||
*
|
||||
* const shouldSkip = entry.header.name.endsWith('.md');
|
||||
* if (shouldSkip) {
|
||||
* // You MUST drain the body with cancel() to proceed to the next entry or read it fully,
|
||||
* // otherwise the stream will stall.
|
||||
* await entry.body.cancel();
|
||||
* continue;
|
||||
* }
|
||||
*
|
||||
* const reader = entry.body.getReader();
|
||||
* while (true) {
|
||||
* const { done, value } = await reader.read();
|
||||
* if (done) break;
|
||||
* processChunk(value);
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
declare function createTarDecoder(options?: DecoderOptions): TransformStream<Uint8Array, ParsedTarEntry>;
|
||||
//#endregion
|
||||
export { type DecoderOptions, type ParsedTarEntry, type ParsedTarEntryWithData, type TarEntry, type TarEntryData, type TarHeader, type TarPackController, type UnpackOptions, createGzipDecoder, createGzipEncoder, createTarDecoder, createTarPacker, packTar, unpackTar };
|
||||
281
node_modules/modern-tar/dist/web/index.js
generated
vendored
Normal file
281
node_modules/modern-tar/dist/web/index.js
generated
vendored
Normal file
@@ -0,0 +1,281 @@
|
||||
import { a as normalizeBody, i as isBodyless, n as createTarPacker$1, r as transformHeader, t as createUnpacker } from "../unpacker-CPCEF5CT.js";
|
||||
//#region src/web/compression.ts
|
||||
function createGzipEncoder() {
|
||||
return new CompressionStream("gzip");
|
||||
}
|
||||
function createGzipDecoder() {
|
||||
return new DecompressionStream("gzip");
|
||||
}
|
||||
//#endregion
|
||||
//#region src/web/pack.ts
|
||||
function createTarPacker() {
|
||||
let streamController;
|
||||
let packer;
|
||||
return {
|
||||
readable: new ReadableStream({ start(controller) {
|
||||
streamController = controller;
|
||||
packer = createTarPacker$1(controller.enqueue.bind(controller), controller.error.bind(controller), controller.close.bind(controller));
|
||||
} }),
|
||||
controller: {
|
||||
add(header) {
|
||||
const bodyless = isBodyless(header);
|
||||
packer.add(header);
|
||||
if (bodyless) packer.endEntry();
|
||||
return new WritableStream({
|
||||
write(chunk) {
|
||||
packer.write(chunk);
|
||||
},
|
||||
close() {
|
||||
if (!bodyless) packer.endEntry();
|
||||
},
|
||||
abort(reason) {
|
||||
streamController.error(reason);
|
||||
}
|
||||
});
|
||||
},
|
||||
finalize() {
|
||||
packer.finalize();
|
||||
},
|
||||
error(err) {
|
||||
streamController.error(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
//#region src/web/stream-utils.ts
|
||||
async function streamToBuffer(stream) {
|
||||
const chunks = [];
|
||||
const reader = stream.getReader();
|
||||
let totalLength = 0;
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
totalLength += value.length;
|
||||
}
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
const drain = (stream) => stream.pipeTo(new WritableStream());
|
||||
//#endregion
|
||||
//#region src/web/unpack.ts
|
||||
function createTarDecoder(options = {}) {
|
||||
const unpacker = createUnpacker(options);
|
||||
const strict = options.strict ?? false;
|
||||
let controller = null;
|
||||
let bodyController = null;
|
||||
let pumping = false;
|
||||
let eofReached = false;
|
||||
let sourceEnded = false;
|
||||
let closed = false;
|
||||
const closeBody = () => {
|
||||
try {
|
||||
bodyController?.close();
|
||||
} catch {}
|
||||
bodyController = null;
|
||||
};
|
||||
const fail = (reason) => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
try {
|
||||
bodyController?.error(reason);
|
||||
} catch {}
|
||||
bodyController = null;
|
||||
try {
|
||||
controller.error(reason);
|
||||
} catch {}
|
||||
controller = null;
|
||||
};
|
||||
const finish = () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
closeBody();
|
||||
try {
|
||||
controller.close();
|
||||
} catch {}
|
||||
controller = null;
|
||||
};
|
||||
const truncateOrFinish = () => {
|
||||
if (strict) throw new Error("Tar archive is truncated.");
|
||||
finish();
|
||||
};
|
||||
const pump = () => {
|
||||
if (pumping || closed || !controller) return;
|
||||
pumping = true;
|
||||
try {
|
||||
while (true) {
|
||||
if (eofReached) {
|
||||
if (sourceEnded) {
|
||||
unpacker.validateEOF();
|
||||
finish();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (unpacker.isEntryActive()) {
|
||||
if (sourceEnded && !unpacker.canFinish()) {
|
||||
truncateOrFinish();
|
||||
break;
|
||||
}
|
||||
if (bodyController) {
|
||||
if ((bodyController.desiredSize ?? 1) <= 0) break;
|
||||
if (unpacker.streamBody((c) => (bodyController.enqueue(c), (bodyController.desiredSize ?? 1) > 0)) === 0 && !unpacker.isBodyComplete()) {
|
||||
if (sourceEnded) truncateOrFinish();
|
||||
break;
|
||||
}
|
||||
} else if (!unpacker.skipEntry()) {
|
||||
if (sourceEnded) truncateOrFinish();
|
||||
break;
|
||||
}
|
||||
if (unpacker.isBodyComplete()) {
|
||||
closeBody();
|
||||
if (!unpacker.skipPadding()) {
|
||||
if (sourceEnded) truncateOrFinish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((controller.desiredSize ?? 0) < 0) break;
|
||||
const header = unpacker.readHeader();
|
||||
if (header === null) {
|
||||
if (sourceEnded) finish();
|
||||
break;
|
||||
}
|
||||
if (header === void 0) {
|
||||
if (sourceEnded) {
|
||||
unpacker.validateEOF();
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
eofReached = true;
|
||||
break;
|
||||
}
|
||||
controller.enqueue({
|
||||
header,
|
||||
body: new ReadableStream({
|
||||
start(c) {
|
||||
if (header.size === 0) c.close();
|
||||
else bodyController = c;
|
||||
},
|
||||
pull: pump,
|
||||
cancel() {
|
||||
bodyController = null;
|
||||
pump();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
throw error;
|
||||
} finally {
|
||||
pumping = false;
|
||||
}
|
||||
};
|
||||
return {
|
||||
readable: new ReadableStream({
|
||||
start(c) {
|
||||
controller = c;
|
||||
},
|
||||
pull: pump,
|
||||
cancel(reason) {
|
||||
if (reason !== void 0) fail(reason);
|
||||
else finish();
|
||||
}
|
||||
}, { highWaterMark: 2 }),
|
||||
writable: new WritableStream({
|
||||
write(chunk) {
|
||||
try {
|
||||
if (eofReached && strict && chunk.some((byte) => byte !== 0)) throw new Error("Invalid EOF.");
|
||||
unpacker.write(chunk);
|
||||
pump();
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
close() {
|
||||
try {
|
||||
sourceEnded = true;
|
||||
unpacker.end();
|
||||
pump();
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
abort(reason) {
|
||||
fail(reason);
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
//#region src/web/helpers.ts
|
||||
async function packTar(entries) {
|
||||
const { readable, controller } = createTarPacker();
|
||||
await (async () => {
|
||||
for (const entry of entries) {
|
||||
const entryStream = controller.add(entry.header);
|
||||
const body = "body" in entry ? entry.body : entry.data;
|
||||
if (!body) {
|
||||
await entryStream.close();
|
||||
continue;
|
||||
}
|
||||
if (body instanceof ReadableStream) await body.pipeTo(entryStream);
|
||||
else if (body instanceof Blob) await body.stream().pipeTo(entryStream);
|
||||
else try {
|
||||
const chunk = await normalizeBody(body);
|
||||
if (chunk.length > 0) {
|
||||
const writer = entryStream.getWriter();
|
||||
await writer.write(chunk);
|
||||
await writer.close();
|
||||
} else await entryStream.close();
|
||||
} catch {
|
||||
throw new TypeError(`Unsupported content type for entry "${entry.header.name}".`);
|
||||
}
|
||||
}
|
||||
})().then(() => controller.finalize()).catch((err) => controller.error(err));
|
||||
return new Uint8Array(await streamToBuffer(readable));
|
||||
}
|
||||
async function unpackTar(archive, options = {}) {
|
||||
const sourceStream = archive instanceof ReadableStream ? archive : new ReadableStream({ start(controller) {
|
||||
controller.enqueue(archive instanceof Uint8Array ? archive : new Uint8Array(archive));
|
||||
controller.close();
|
||||
} });
|
||||
const results = [];
|
||||
const entryStream = sourceStream.pipeThrough(createTarDecoder(options));
|
||||
for await (const entry of entryStream) {
|
||||
let processedHeader;
|
||||
try {
|
||||
processedHeader = transformHeader(entry.header, options);
|
||||
} catch (error) {
|
||||
await entry.body.cancel();
|
||||
throw error;
|
||||
}
|
||||
if (processedHeader === null) {
|
||||
await drain(entry.body);
|
||||
continue;
|
||||
}
|
||||
if (isBodyless(processedHeader)) {
|
||||
await drain(entry.body);
|
||||
results.push({ header: processedHeader });
|
||||
} else results.push({
|
||||
header: processedHeader,
|
||||
data: await streamToBuffer(entry.body)
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
//#endregion
|
||||
export { createGzipDecoder, createGzipEncoder, createTarDecoder, createTarPacker, packTar, unpackTar };
|
||||
65
node_modules/modern-tar/package.json
generated
vendored
Normal file
65
node_modules/modern-tar/package.json
generated
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "modern-tar",
|
||||
"version": "0.7.6",
|
||||
"description": "Zero dependency streaming tar parser and writer for JavaScript.",
|
||||
"author": "Ayuhito <hello@ayuhito.com>",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "./dist/web/index.js",
|
||||
"module": "./dist/web/index.js",
|
||||
"types": "./dist/web/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": "./dist/web/index.js",
|
||||
"./fs": "./dist/fs/index.js"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"fs": [
|
||||
"dist/fs/index.d.ts"
|
||||
],
|
||||
"*": [
|
||||
"dist/web/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.7",
|
||||
"@types/node": "^25.5.0",
|
||||
"@vitest/browser-playwright": "4.1.0",
|
||||
"@vitest/coverage-v8": "4.1.0",
|
||||
"miniflare": "^4.20260312.0",
|
||||
"tsdown": "^0.21.3",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "4.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"dev": "tsdown --watch",
|
||||
"test": "vitest",
|
||||
"test:workers": "tsdown && vitest --config vitest.workers.config.ts --run",
|
||||
"coverage": "vitest run --coverage",
|
||||
"check": "biome check --write",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:browser": "vitest --config=vitest.browser.config.ts --browser"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"homepage": "https://github.com/ayuhito/modern-tar",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ayuhito/modern-tar/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ayuhito/modern-tar.git"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user