exarch-rs 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +1 -1
- package/README.md +25 -9
- package/native/exarch-rs.darwin-arm64.node +0 -0
- package/native/exarch-rs.darwin-x64.node +0 -0
- package/native/exarch-rs.linux-arm64-gnu.node +0 -0
- package/native/exarch-rs.linux-x64-gnu.node +0 -0
- package/native/exarch-rs.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
- package/src/lib.rs +7 -3
- package/tests/create.test.js +13 -1
- package/tests/extract.test.js +31 -32
- package/tests/list-verify.test.js +25 -1
- package/exarch-rs.darwin-arm64.node +0 -0
- package/index.d.ts +0 -657
- package/index.js +0 -588
package/Cargo.toml
CHANGED
|
@@ -17,7 +17,7 @@ crate-type = ["cdylib"]
|
|
|
17
17
|
exarch-core.workspace = true
|
|
18
18
|
napi = { workspace = true, features = ["async", "napi4", "error_anyhow", "tokio_rt"] }
|
|
19
19
|
napi-derive.workspace = true
|
|
20
|
-
tokio
|
|
20
|
+
tokio = { workspace = true, features = ["rt"] }
|
|
21
21
|
|
|
22
22
|
[build-dependencies]
|
|
23
23
|
napi-build.workspace = true
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://github.com/bug-ops/exarch/actions)
|
|
7
7
|
[](../../LICENSE-MIT)
|
|
8
8
|
|
|
9
|
-
Memory-safe archive extraction library for Node.js.
|
|
9
|
+
Memory-safe archive extraction and creation library for Node.js.
|
|
10
10
|
|
|
11
11
|
> [!IMPORTANT]
|
|
12
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.
|
|
@@ -38,6 +38,8 @@ bun add exarch-rs
|
|
|
38
38
|
|
|
39
39
|
## Quick Start
|
|
40
40
|
|
|
41
|
+
### Extraction
|
|
42
|
+
|
|
41
43
|
```javascript
|
|
42
44
|
const { extractArchive } = require('exarch-rs');
|
|
43
45
|
|
|
@@ -46,6 +48,16 @@ const result = await extractArchive('archive.tar.gz', '/output/path');
|
|
|
46
48
|
console.log(`Extracted ${result.filesExtracted} files`);
|
|
47
49
|
```
|
|
48
50
|
|
|
51
|
+
### Creation
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
const { createArchive } = require('exarch-rs');
|
|
55
|
+
|
|
56
|
+
// Async (recommended)
|
|
57
|
+
const result = await createArchive('backup.tar.gz', ['src/', 'package.json']);
|
|
58
|
+
console.log(`Created archive with ${result.filesAdded} files`);
|
|
59
|
+
```
|
|
60
|
+
|
|
49
61
|
## Usage
|
|
50
62
|
|
|
51
63
|
### Async API (Recommended)
|
|
@@ -178,14 +190,18 @@ The library provides built-in protection against:
|
|
|
178
190
|
|
|
179
191
|
## Supported Formats
|
|
180
192
|
|
|
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` |
|
|
193
|
+
| Format | Extensions | Extract | Create |
|
|
194
|
+
|--------|------------|:-------:|:------:|
|
|
195
|
+
| TAR | `.tar` | ✅ | ✅ |
|
|
196
|
+
| TAR+GZIP | `.tar.gz`, `.tgz` | ✅ | ✅ |
|
|
197
|
+
| TAR+BZIP2 | `.tar.bz2`, `.tbz2` | ✅ | ✅ |
|
|
198
|
+
| TAR+XZ | `.tar.xz`, `.txz` | ✅ | ✅ |
|
|
199
|
+
| TAR+ZSTD | `.tar.zst`, `.tzst` | ✅ | ✅ |
|
|
200
|
+
| ZIP | `.zip` | ✅ | ✅ |
|
|
201
|
+
| 7z | `.7z` | ✅ | — |
|
|
202
|
+
|
|
203
|
+
> [!NOTE]
|
|
204
|
+
> 7z creation is not yet supported. Solid and encrypted 7z archives are rejected for security reasons.
|
|
189
205
|
|
|
190
206
|
## Comparison with tar-fs
|
|
191
207
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/src/lib.rs
CHANGED
|
@@ -208,9 +208,13 @@ pub fn extract_archive_sync(
|
|
|
208
208
|
let default_config = exarch_core::SecurityConfig::default();
|
|
209
209
|
let config_ref = config.map_or(&default_config, |c| c.as_core());
|
|
210
210
|
|
|
211
|
-
// Run extraction synchronously
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
// Run extraction synchronously with panic safety
|
|
212
|
+
// CRITICAL: Never panic across FFI boundary
|
|
213
|
+
let report = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
214
|
+
exarch_core::extract_archive(&archive_path, &output_dir, config_ref)
|
|
215
|
+
}))
|
|
216
|
+
.map_err(|_| Error::from_reason("Internal panic during archive extraction"))?
|
|
217
|
+
.map_err(convert_error)?;
|
|
214
218
|
|
|
215
219
|
Ok(ExtractionReport::from(report))
|
|
216
220
|
}
|
package/tests/create.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for archive creation functions
|
|
3
3
|
*/
|
|
4
|
-
const { describe, it, beforeEach } = require('node:test');
|
|
4
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
5
5
|
const assert = require('node:assert');
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const path = require('node:path');
|
|
@@ -39,6 +39,12 @@ describe('createArchive (async)', () => {
|
|
|
39
39
|
outputPath = path.join(tempDir, 'output.tar.gz');
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
44
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
42
48
|
it('should create a tar.gz archive', async () => {
|
|
43
49
|
const report = await createArchive(outputPath, [sourceDir]);
|
|
44
50
|
|
|
@@ -95,6 +101,12 @@ describe('createArchiveSync', () => {
|
|
|
95
101
|
outputPath = path.join(tempDir, 'output.tar.gz');
|
|
96
102
|
});
|
|
97
103
|
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
106
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
98
110
|
it('should create archive synchronously', () => {
|
|
99
111
|
const report = createArchiveSync(outputPath, [sourceDir]);
|
|
100
112
|
|
package/tests/extract.test.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for archive extraction functions
|
|
3
|
-
*
|
|
4
|
-
* NOTE: Extraction tests are skipped until exarch-core extract_archive API is fully implemented.
|
|
5
|
-
* The current implementation is a placeholder (see exarch-core/src/api.rs).
|
|
6
3
|
*/
|
|
7
|
-
const { describe, it, beforeEach } = require('node:test');
|
|
4
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
8
5
|
const assert = require('node:assert');
|
|
9
6
|
const fs = require('node:fs');
|
|
10
7
|
const path = require('node:path');
|
|
@@ -42,19 +39,29 @@ describe('extractArchive (async)', () => {
|
|
|
42
39
|
fs.mkdirSync(outputDir);
|
|
43
40
|
});
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
44
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should extract a valid archive', async () => {
|
|
47
49
|
createValidArchive(archivePath, tempDir);
|
|
48
50
|
|
|
49
51
|
const report = await extractArchive(archivePath, outputDir);
|
|
50
52
|
|
|
51
|
-
assert.
|
|
53
|
+
assert.strictEqual(report.filesExtracted, 1);
|
|
52
54
|
assert.ok(report.bytesWritten >= 13);
|
|
53
55
|
assert.ok(report.durationMs >= 0);
|
|
56
|
+
|
|
57
|
+
// Verify extracted file exists and has correct content
|
|
58
|
+
const extractedFile = path.join(outputDir, 'hello.txt');
|
|
59
|
+
assert.ok(fs.existsSync(extractedFile), 'Extracted file should exist');
|
|
60
|
+
const content = fs.readFileSync(extractedFile, 'utf8');
|
|
61
|
+
assert.strictEqual(content, 'Hello, World!');
|
|
54
62
|
});
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
it.skip('should accept custom SecurityConfig', async () => {
|
|
64
|
+
it('should accept custom SecurityConfig', async () => {
|
|
58
65
|
createValidArchive(archivePath, tempDir);
|
|
59
66
|
|
|
60
67
|
const config = new SecurityConfig();
|
|
@@ -63,15 +70,6 @@ describe('extractArchive (async)', () => {
|
|
|
63
70
|
|
|
64
71
|
assert.ok(report.filesExtracted >= 1);
|
|
65
72
|
});
|
|
66
|
-
|
|
67
|
-
it('should return empty report for valid archive (placeholder)', async () => {
|
|
68
|
-
createValidArchive(archivePath, tempDir);
|
|
69
|
-
|
|
70
|
-
const report = await extractArchive(archivePath, outputDir);
|
|
71
|
-
|
|
72
|
-
// Core extract_archive is currently a placeholder
|
|
73
|
-
assert.strictEqual(report.filesExtracted, 0);
|
|
74
|
-
});
|
|
75
73
|
});
|
|
76
74
|
|
|
77
75
|
describe('extractArchiveSync', () => {
|
|
@@ -86,18 +84,28 @@ describe('extractArchiveSync', () => {
|
|
|
86
84
|
fs.mkdirSync(outputDir);
|
|
87
85
|
});
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
89
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should extract a valid archive synchronously', () => {
|
|
91
94
|
createValidArchive(archivePath, tempDir);
|
|
92
95
|
|
|
93
96
|
const report = extractArchiveSync(archivePath, outputDir);
|
|
94
97
|
|
|
95
|
-
assert.
|
|
98
|
+
assert.strictEqual(report.filesExtracted, 1);
|
|
96
99
|
assert.ok(report.bytesWritten >= 13);
|
|
100
|
+
|
|
101
|
+
// Verify extracted file exists and has correct content
|
|
102
|
+
const extractedFile = path.join(outputDir, 'hello.txt');
|
|
103
|
+
assert.ok(fs.existsSync(extractedFile), 'Extracted file should exist');
|
|
104
|
+
const content = fs.readFileSync(extractedFile, 'utf8');
|
|
105
|
+
assert.strictEqual(content, 'Hello, World!');
|
|
97
106
|
});
|
|
98
107
|
|
|
99
|
-
|
|
100
|
-
it.skip('should accept custom SecurityConfig', () => {
|
|
108
|
+
it('should accept custom SecurityConfig', () => {
|
|
101
109
|
createValidArchive(archivePath, tempDir);
|
|
102
110
|
|
|
103
111
|
const config = new SecurityConfig();
|
|
@@ -106,13 +114,4 @@ describe('extractArchiveSync', () => {
|
|
|
106
114
|
|
|
107
115
|
assert.ok(report.filesExtracted >= 1);
|
|
108
116
|
});
|
|
109
|
-
|
|
110
|
-
it('should return empty report for valid archive (placeholder)', () => {
|
|
111
|
-
createValidArchive(archivePath, tempDir);
|
|
112
|
-
|
|
113
|
-
const report = extractArchiveSync(archivePath, outputDir);
|
|
114
|
-
|
|
115
|
-
// Core extract_archive is currently a placeholder
|
|
116
|
-
assert.strictEqual(report.filesExtracted, 0);
|
|
117
|
-
});
|
|
118
117
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for listArchive and verifyArchive functions
|
|
3
3
|
*/
|
|
4
|
-
const { describe, it, beforeEach } = require('node:test');
|
|
4
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
5
5
|
const assert = require('node:assert');
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const path = require('node:path');
|
|
@@ -38,6 +38,12 @@ describe('listArchive (async)', () => {
|
|
|
38
38
|
archivePath = path.join(tempDir, 'test.tar.gz');
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
43
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
41
47
|
it('should list archive contents', async () => {
|
|
42
48
|
createValidArchive(archivePath, tempDir);
|
|
43
49
|
|
|
@@ -78,6 +84,12 @@ describe('listArchiveSync', () => {
|
|
|
78
84
|
archivePath = path.join(tempDir, 'test.tar.gz');
|
|
79
85
|
});
|
|
80
86
|
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
89
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
81
93
|
it('should list archive contents synchronously', () => {
|
|
82
94
|
createValidArchive(archivePath, tempDir);
|
|
83
95
|
|
|
@@ -98,6 +110,12 @@ describe('verifyArchive (async)', () => {
|
|
|
98
110
|
archivePath = path.join(tempDir, 'test.tar.gz');
|
|
99
111
|
});
|
|
100
112
|
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
115
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
101
119
|
it('should verify a valid archive', async () => {
|
|
102
120
|
createValidArchive(archivePath, tempDir);
|
|
103
121
|
|
|
@@ -137,6 +155,12 @@ describe('verifyArchiveSync', () => {
|
|
|
137
155
|
archivePath = path.join(tempDir, 'test.tar.gz');
|
|
138
156
|
});
|
|
139
157
|
|
|
158
|
+
afterEach(() => {
|
|
159
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
160
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
140
164
|
it('should verify archive synchronously', () => {
|
|
141
165
|
createValidArchive(archivePath, tempDir);
|
|
142
166
|
|
|
Binary file
|