exarch-rs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml ADDED
@@ -0,0 +1,28 @@
1
+ [package]
2
+ name = "exarch-node"
3
+ description = "Node.js bindings for exarch-core"
4
+ version.workspace = true
5
+ authors.workspace = true
6
+ edition.workspace = true
7
+ rust-version.workspace = true
8
+ license.workspace = true
9
+ repository.workspace = true
10
+ homepage.workspace = true
11
+
12
+ [lib]
13
+ crate-type = ["cdylib"]
14
+
15
+ [dependencies]
16
+ exarch-core.workspace = true
17
+ napi = { workspace = true, features = ["async", "napi4", "error_anyhow", "tokio_rt"] }
18
+ napi-derive.workspace = true
19
+ tokio.workspace = true
20
+
21
+ [build-dependencies]
22
+ napi-build.workspace = true
23
+
24
+ [dev-dependencies]
25
+ tokio = { workspace = true, features = ["macros", "rt"] }
26
+
27
+ [lints]
28
+ workspace = true
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # exarch-rs
2
+
3
+ [![npm](https://img.shields.io/npm/v/exarch-rs)](https://www.npmjs.com/package/exarch-rs)
4
+ [![Node](https://img.shields.io/node/v/exarch-rs)](https://nodejs.org)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue)](https://www.typescriptlang.org/)
6
+ [![CI](https://img.shields.io/github/actions/workflow/status/bug-ops/exarch/ci.yml?branch=main)](https://github.com/bug-ops/exarch/actions)
7
+ [![License](https://img.shields.io/npm/l/exarch-rs)](../../LICENSE-MIT)
8
+
9
+ Memory-safe archive extraction library for Node.js.
10
+
11
+ > [!IMPORTANT]
12
+ > **exarch** is designed as a secure replacement for vulnerable archive libraries like `tar-fs`, which has known CVEs with CVSS scores up to 9.4.
13
+
14
+ This package provides Node.js bindings for [exarch-core](../exarch-core), a Rust library with built-in protection against common archive vulnerabilities.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ # npm
20
+ npm install exarch-rs
21
+
22
+ # yarn
23
+ yarn add exarch-rs
24
+
25
+ # pnpm
26
+ pnpm add exarch-rs
27
+
28
+ # bun
29
+ bun add exarch-rs
30
+ ```
31
+
32
+ > [!NOTE]
33
+ > This package includes TypeScript definitions. No need for separate `@types` package.
34
+
35
+ ## Requirements
36
+
37
+ - Node.js >= 14
38
+
39
+ ## Quick Start
40
+
41
+ ```javascript
42
+ const { extractArchive } = require('exarch-rs');
43
+
44
+ // Async (recommended)
45
+ const result = await extractArchive('archive.tar.gz', '/output/path');
46
+ console.log(`Extracted ${result.filesExtracted} files`);
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ### Async API (Recommended)
52
+
53
+ ```javascript
54
+ const { extractArchive } = require('exarch-rs');
55
+
56
+ const result = await extractArchive('archive.tar.gz', '/output/path');
57
+
58
+ console.log(`Files extracted: ${result.filesExtracted}`);
59
+ console.log(`Bytes written: ${result.bytesWritten}`);
60
+ console.log(`Duration: ${result.durationMs}ms`);
61
+ ```
62
+
63
+ ### Sync API
64
+
65
+ ```javascript
66
+ const { extractArchiveSync } = require('exarch-rs');
67
+
68
+ const result = extractArchiveSync('archive.tar.gz', '/output/path');
69
+ console.log(`Extracted ${result.filesExtracted} files`);
70
+ ```
71
+
72
+ > [!TIP]
73
+ > Prefer the async API to avoid blocking the event loop during extraction.
74
+
75
+ ### ES Modules
76
+
77
+ ```javascript
78
+ import { extractArchive } from 'exarch-rs';
79
+
80
+ const result = await extractArchive('archive.tar.gz', '/output/path');
81
+ ```
82
+
83
+ ### TypeScript
84
+
85
+ ```typescript
86
+ import { extractArchive, SecurityConfig, ExtractionReport } from 'exarch-rs';
87
+
88
+ const result: ExtractionReport = await extractArchive('archive.tar.gz', '/output/path');
89
+ console.log(`Extracted ${result.filesExtracted} files`);
90
+ ```
91
+
92
+ ### Custom Security Configuration
93
+
94
+ ```typescript
95
+ import { extractArchive, SecurityConfig } from 'exarch-rs';
96
+
97
+ const config = new SecurityConfig()
98
+ .maxFileSize(100 * 1024 * 1024) // 100 MB per file
99
+ .maxTotalSize(1024 * 1024 * 1024) // 1 GB total
100
+ .maxFileCount(10_000); // Max 10k files
101
+
102
+ const result = await extractArchive('archive.tar.gz', '/output', config);
103
+ ```
104
+
105
+ ### Error Handling
106
+
107
+ ```javascript
108
+ const { extractArchive } = require('exarch-rs');
109
+
110
+ try {
111
+ const result = await extractArchive('archive.tar.gz', '/output');
112
+ console.log(`Success: ${result.filesExtracted} files`);
113
+ } catch (error) {
114
+ // Error codes: PATH_TRAVERSAL, SYMLINK_ESCAPE, ZIP_BOMB, QUOTA_EXCEEDED, etc.
115
+ console.error(`Extraction failed: ${error.message}`);
116
+ }
117
+ ```
118
+
119
+ ## API
120
+
121
+ ### `extractArchive(archivePath, outputDir, config?)`
122
+
123
+ Extract an archive asynchronously with security validation.
124
+
125
+ **Parameters:**
126
+
127
+ | Name | Type | Description |
128
+ |------|------|-------------|
129
+ | `archivePath` | `string` | Path to the archive file |
130
+ | `outputDir` | `string` | Directory where files will be extracted |
131
+ | `config` | `SecurityConfig` | Optional security configuration |
132
+
133
+ **Returns:** `Promise<ExtractionReport>`
134
+
135
+ ### `extractArchiveSync(archivePath, outputDir, config?)`
136
+
137
+ Synchronous version. Blocks the event loop until extraction completes.
138
+
139
+ **Returns:** `ExtractionReport`
140
+
141
+ ### `ExtractionReport`
142
+
143
+ ```typescript
144
+ interface ExtractionReport {
145
+ filesExtracted: number; // Number of files extracted
146
+ bytesWritten: number; // Total bytes written
147
+ durationMs: number; // Extraction duration in milliseconds
148
+ }
149
+ ```
150
+
151
+ ### `SecurityConfig`
152
+
153
+ Builder-style security configuration.
154
+
155
+ ```typescript
156
+ const config = new SecurityConfig()
157
+ .maxFileSize(bytes) // Max size per file
158
+ .maxTotalSize(bytes) // Max total extraction size
159
+ .maxFileCount(count) // Max number of files
160
+ .maxCompressionRatio(n); // Max compression ratio (zip bomb detection)
161
+ ```
162
+
163
+ ## Security Features
164
+
165
+ The library provides built-in protection against:
166
+
167
+ | Protection | Description |
168
+ |------------|-------------|
169
+ | Path traversal | Blocks `../` and absolute paths |
170
+ | Symlink attacks | Prevents symlinks escaping extraction directory |
171
+ | Hardlink attacks | Validates hardlink targets |
172
+ | Zip bombs | Detects high compression ratios |
173
+ | Permission sanitization | Strips setuid/setgid bits |
174
+ | Size limits | Enforces file and total size limits |
175
+
176
+ > [!CAUTION]
177
+ > Unlike many Node.js archive libraries, exarch applies security validation by default.
178
+
179
+ ## Supported Formats
180
+
181
+ | Format | Extensions |
182
+ |--------|------------|
183
+ | TAR | `.tar` |
184
+ | TAR+GZIP | `.tar.gz`, `.tgz` |
185
+ | TAR+BZIP2 | `.tar.bz2`, `.tbz2` |
186
+ | TAR+XZ | `.tar.xz`, `.txz` |
187
+ | TAR+ZSTD | `.tar.zst`, `.tzst` |
188
+ | ZIP | `.zip` |
189
+
190
+ ## Comparison with tar-fs
191
+
192
+ ```javascript
193
+ // UNSAFE - tar-fs has known vulnerabilities
194
+ const tar = require('tar-fs');
195
+ const fs = require('fs');
196
+ fs.createReadStream('archive.tar')
197
+ .pipe(tar.extract('/output')); // May extract outside target directory!
198
+
199
+ // SAFE - exarch-rs validates all paths
200
+ const { extractArchive } = require('exarch-rs');
201
+ await extractArchive('archive.tar', '/output'); // Protected by default
202
+ ```
203
+
204
+ ## Development
205
+
206
+ This package is built using [napi-rs](https://napi.rs/).
207
+
208
+ ```bash
209
+ # Clone repository
210
+ git clone https://github.com/bug-ops/exarch
211
+ cd exarch/crates/exarch-node
212
+
213
+ # Install dependencies
214
+ npm install
215
+
216
+ # Build native module
217
+ npm run build
218
+
219
+ # Run tests
220
+ npm test
221
+ ```
222
+
223
+ ## Related Packages
224
+
225
+ - [exarch-core](../exarch-core) — Core Rust library
226
+ - [exarch (PyPI)](../exarch-python) — Python bindings
227
+
228
+ ## License
229
+
230
+ Licensed under either of:
231
+
232
+ - Apache License, Version 2.0 ([LICENSE-APACHE](../../LICENSE-APACHE))
233
+ - MIT License ([LICENSE-MIT](../../LICENSE-MIT))
234
+
235
+ at your option.
package/build.rs ADDED
@@ -0,0 +1,5 @@
1
+ //! Build script for napi-rs bindings.
2
+
3
+ fn main() {
4
+ napi_build::setup();
5
+ }
package/index.d.ts ADDED
@@ -0,0 +1,287 @@
1
+ /**
2
+ * exarch - Memory-safe archive extraction library
3
+ *
4
+ * Provides secure archive extraction with built-in protection against:
5
+ * - Path traversal attacks
6
+ * - Symlink escape attacks
7
+ * - Hardlink escape attacks
8
+ * - Zip bomb attacks
9
+ * - Invalid file permissions
10
+ * - Resource quota violations
11
+ */
12
+
13
+ /**
14
+ * Security configuration for archive extraction.
15
+ *
16
+ * All security features default to deny (secure-by-default policy).
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * // Use secure defaults
21
+ * const config = new SecurityConfig();
22
+ *
23
+ * // Customize with builder pattern
24
+ * const config = new SecurityConfig()
25
+ * .maxFileSize(100 * 1024 * 1024)
26
+ * .allowSymlinks(true);
27
+ *
28
+ * // Use permissive configuration for trusted archives
29
+ * const config = SecurityConfig.permissive();
30
+ * ```
31
+ */
32
+ export class SecurityConfig {
33
+ /**
34
+ * Creates a new SecurityConfig with secure defaults.
35
+ */
36
+ constructor();
37
+
38
+ /**
39
+ * Creates a SecurityConfig with secure defaults.
40
+ * Equivalent to calling the constructor.
41
+ */
42
+ static default(): SecurityConfig;
43
+
44
+ /**
45
+ * Creates a permissive configuration for trusted archives.
46
+ *
47
+ * Enables: symlinks, hardlinks, absolute paths, world-writable files.
48
+ * Use only for archives from trusted sources.
49
+ */
50
+ static permissive(): SecurityConfig;
51
+
52
+ // Builder pattern methods (chainable)
53
+
54
+ /**
55
+ * Sets the maximum file size in bytes.
56
+ * Default: 50 MB (52,428,800 bytes)
57
+ */
58
+ maxFileSize(size: number): this;
59
+
60
+ /**
61
+ * Sets the maximum total size in bytes.
62
+ * Default: 500 MB (524,288,000 bytes)
63
+ */
64
+ maxTotalSize(size: number): this;
65
+
66
+ /**
67
+ * Sets the maximum compression ratio.
68
+ * Default: 100.0
69
+ *
70
+ * @throws Error if ratio is not a positive finite number
71
+ */
72
+ maxCompressionRatio(ratio: number): this;
73
+
74
+ /**
75
+ * Sets the maximum file count.
76
+ * Default: 10,000
77
+ */
78
+ maxFileCount(count: number): this;
79
+
80
+ /**
81
+ * Sets the maximum path depth.
82
+ * Default: 32
83
+ */
84
+ maxPathDepth(depth: number): this;
85
+
86
+ /**
87
+ * Allows or denies symlinks.
88
+ * Default: false (deny)
89
+ */
90
+ allowSymlinks(allow?: boolean): this;
91
+
92
+ /**
93
+ * Allows or denies hardlinks.
94
+ * Default: false (deny)
95
+ */
96
+ allowHardlinks(allow?: boolean): this;
97
+
98
+ /**
99
+ * Allows or denies absolute paths.
100
+ * Default: false (deny)
101
+ */
102
+ allowAbsolutePaths(allow?: boolean): this;
103
+
104
+ /**
105
+ * Allows or denies world-writable files.
106
+ * Default: false (deny)
107
+ */
108
+ allowWorldWritable(allow?: boolean): this;
109
+
110
+ /**
111
+ * Sets whether to preserve permissions from archive.
112
+ * Default: false
113
+ */
114
+ preservePermissions(preserve?: boolean): this;
115
+
116
+ /**
117
+ * Adds an allowed file extension.
118
+ *
119
+ * @throws Error if extension exceeds maximum length or contains null bytes
120
+ */
121
+ addAllowedExtension(ext: string): this;
122
+
123
+ /**
124
+ * Adds a banned path component.
125
+ *
126
+ * @throws Error if component exceeds maximum length or contains null bytes
127
+ */
128
+ addBannedComponent(component: string): this;
129
+
130
+ /**
131
+ * Finalizes the configuration (for API consistency).
132
+ *
133
+ * This method is provided for builder pattern consistency but is optional.
134
+ * The configuration is always valid and can be used directly.
135
+ */
136
+ build(): this;
137
+
138
+ // Validation methods
139
+
140
+ /**
141
+ * Checks if a path component is allowed.
142
+ */
143
+ isPathComponentAllowed(component: string): boolean;
144
+
145
+ /**
146
+ * Checks if a file extension is allowed.
147
+ */
148
+ isExtensionAllowed(extension: string): boolean;
149
+
150
+ // Property getters
151
+
152
+ /** Maximum file size in bytes */
153
+ readonly maxFileSize: number;
154
+
155
+ /** Maximum total extraction size in bytes */
156
+ readonly maxTotalSize: number;
157
+
158
+ /** Maximum compression ratio */
159
+ readonly maxCompressionRatio: number;
160
+
161
+ /** Maximum number of files */
162
+ readonly maxFileCount: number;
163
+
164
+ /** Maximum path depth */
165
+ readonly maxPathDepth: number;
166
+
167
+ /** Whether file permissions are preserved from archive */
168
+ readonly preservePermissions: boolean;
169
+
170
+ /** Whether symlinks are allowed */
171
+ readonly allowSymlinks: boolean;
172
+
173
+ /** Whether hardlinks are allowed */
174
+ readonly allowHardlinks: boolean;
175
+
176
+ /** Whether absolute paths are allowed */
177
+ readonly allowAbsolutePaths: boolean;
178
+
179
+ /** Whether world-writable files are allowed */
180
+ readonly allowWorldWritable: boolean;
181
+
182
+ /** List of allowed file extensions */
183
+ readonly allowedExtensions: string[];
184
+
185
+ /** List of banned path components */
186
+ readonly bannedPathComponents: string[];
187
+ }
188
+
189
+ /**
190
+ * Report of an archive extraction operation.
191
+ *
192
+ * Contains statistics and metadata about the extraction process.
193
+ */
194
+ export interface ExtractionReport {
195
+ /** Number of files successfully extracted */
196
+ filesExtracted: number;
197
+
198
+ /** Number of directories created */
199
+ directoriesCreated: number;
200
+
201
+ /** Number of symlinks created */
202
+ symlinksCreated: number;
203
+
204
+ /** Total bytes written to disk */
205
+ bytesWritten: number;
206
+
207
+ /** Extraction duration in milliseconds */
208
+ durationMs: number;
209
+
210
+ /** Number of files skipped due to security checks */
211
+ filesSkipped: number;
212
+
213
+ /** List of warning messages */
214
+ warnings: string[];
215
+ }
216
+
217
+ /**
218
+ * Extract an archive to the specified directory (async).
219
+ *
220
+ * This function provides secure archive extraction with configurable
221
+ * security policies. By default, it uses a restrictive security
222
+ * configuration that blocks symlinks, hardlinks, absolute paths, and
223
+ * enforces resource quotas.
224
+ *
225
+ * Error codes in exception messages:
226
+ * - `PATH_TRAVERSAL`: Path traversal attempt detected
227
+ * - `SYMLINK_ESCAPE`: Symlink points outside extraction directory
228
+ * - `HARDLINK_ESCAPE`: Hardlink target outside extraction directory
229
+ * - `ZIP_BOMB`: Potential zip bomb detected
230
+ * - `INVALID_PERMISSIONS`: File permissions are invalid or unsafe
231
+ * - `QUOTA_EXCEEDED`: Resource quota exceeded
232
+ * - `SECURITY_VIOLATION`: Security policy violation
233
+ * - `UNSUPPORTED_FORMAT`: Archive format not supported
234
+ * - `INVALID_ARCHIVE`: Archive is corrupted
235
+ * - `IO_ERROR`: I/O operation failed
236
+ *
237
+ * @param archivePath - Path to the archive file
238
+ * @param outputDir - Directory where files will be extracted
239
+ * @param config - Optional SecurityConfig (uses secure defaults if omitted)
240
+ * @returns Promise resolving to ExtractionReport with extraction statistics
241
+ * @throws Error for security violations or I/O errors
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * // Use secure defaults
246
+ * const report = await extractArchive('archive.tar.gz', '/tmp/output');
247
+ * console.log(`Extracted ${report.filesExtracted} files`);
248
+ *
249
+ * // Customize security settings
250
+ * const config = new SecurityConfig().maxFileSize(100 * 1024 * 1024);
251
+ * const report = await extractArchive('archive.tar.gz', '/tmp/output', config);
252
+ * ```
253
+ */
254
+ export function extractArchive(
255
+ archivePath: string,
256
+ outputDir: string,
257
+ config?: SecurityConfig
258
+ ): Promise<ExtractionReport>;
259
+
260
+ /**
261
+ * Extract an archive to the specified directory (sync).
262
+ *
263
+ * Synchronous version of extractArchive. Blocks the event loop until
264
+ * extraction completes. Prefer the async version for most use cases.
265
+ *
266
+ * @param archivePath - Path to the archive file
267
+ * @param outputDir - Directory where files will be extracted
268
+ * @param config - Optional SecurityConfig (uses secure defaults if omitted)
269
+ * @returns ExtractionReport with extraction statistics
270
+ * @throws Error for security violations or I/O errors
271
+ *
272
+ * @example
273
+ * ```typescript
274
+ * // Use secure defaults
275
+ * const report = extractArchiveSync('archive.tar.gz', '/tmp/output');
276
+ * console.log(`Extracted ${report.filesExtracted} files`);
277
+ *
278
+ * // Customize security settings
279
+ * const config = new SecurityConfig().maxFileSize(100 * 1024 * 1024);
280
+ * const report = extractArchiveSync('archive.tar.gz', '/tmp/output', config);
281
+ * ```
282
+ */
283
+ export function extractArchiveSync(
284
+ archivePath: string,
285
+ outputDir: string,
286
+ config?: SecurityConfig
287
+ ): ExtractionReport;
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "exarch-rs",
3
+ "version": "0.1.0",
4
+ "description": "Memory-safe archive extraction library",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "keywords": [
8
+ "archive",
9
+ "extraction",
10
+ "security",
11
+ "tar",
12
+ "zip",
13
+ "napi-rs",
14
+ "rust"
15
+ ],
16
+ "license": "MIT OR Apache-2.0",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/bug-ops/exarch"
20
+ },
21
+ "napi": {
22
+ "name": "exarch-rs",
23
+ "triples": {
24
+ "defaults": true
25
+ }
26
+ },
27
+ "scripts": {
28
+ "build": "napi build --platform --release",
29
+ "build:debug": "napi build --platform"
30
+ },
31
+ "devDependencies": {
32
+ "@napi-rs/cli": "^3.0.0"
33
+ },
34
+ "engines": {
35
+ "node": ">= 14"
36
+ }
37
+ }