rar-stream 4.0.3 โ 5.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/README.md +283 -31
- package/lib/browser.d.ts +63 -0
- package/lib/browser.mjs +132 -0
- package/lib/index.d.ts +119 -0
- package/lib/index.mjs +376 -0
- package/package.json +35 -19
- package/rar-stream.darwin-arm64.node +0 -0
- package/rar-stream.darwin-x64.node +0 -0
- package/rar-stream.linux-arm64-gnu.node +0 -0
- package/rar-stream.linux-x64-gnu.node +0 -0
- package/rar-stream.linux-x64-musl.node +0 -0
- package/rar-stream.win32-x64-msvc.node +0 -0
package/README.md
CHANGED
|
@@ -1,22 +1,46 @@
|
|
|
1
1
|
# rar-stream
|
|
2
2
|
|
|
3
|
-
> Fast RAR archive streaming for Node.js and browsers. Zero dependencies
|
|
3
|
+
> Fast RAR archive streaming for Rust, Node.js, and browsers. Zero dependencies core.
|
|
4
4
|
|
|
5
|
+
[](https://github.com/doom-fish/rar-stream/actions/workflows/ci.yml)
|
|
5
6
|
[](https://www.npmjs.com/package/rar-stream)
|
|
7
|
+
[](https://www.npmjs.com/package/rar-stream)
|
|
8
|
+
[](https://crates.io/crates/rar-stream)
|
|
9
|
+
[](https://crates.io/crates/rar-stream)
|
|
10
|
+
[](https://docs.rs/rar-stream)
|
|
11
|
+
[](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)
|
|
6
12
|
[](https://opensource.org/licenses/MIT)
|
|
7
13
|
|
|
14
|
+
## What's New in v5.1.0
|
|
15
|
+
|
|
16
|
+
- โก **Parallel RAR5 pipeline**: Beats the official C `unrar` in all 24 benchmark scenarios
|
|
17
|
+
- ๐ **WASM RAR5 support**: Full RAR5 decompression in the browser via `WasmRar5Decoder`
|
|
18
|
+
- ๐ **NAPI pipeline**: Node.js users get 40% faster decompression automatically
|
|
19
|
+
- ๐งช **E2E browser tests**: Full Playwright tests for upload โ decompress โ verify workflow
|
|
20
|
+
|
|
8
21
|
## Features
|
|
9
22
|
|
|
10
23
|
- ๐ **Fast**: Native Rust implementation with NAPI bindings
|
|
11
|
-
- ๐ฆ **Zero dependencies**:
|
|
24
|
+
- ๐ฆ **Zero dependencies**: Core library has no external dependencies
|
|
12
25
|
- ๐ **Cross-platform**: Works on Linux, macOS, Windows
|
|
13
26
|
- ๐ **Streaming**: Stream files directly from RAR archives
|
|
14
27
|
- ๐ **Multi-volume**: Supports split archives (.rar, .r00, .r01, ...)
|
|
15
|
-
- ๐๏ธ **Full decompression**: LZSS, PPMd, and
|
|
28
|
+
- ๐๏ธ **Full decompression**: LZSS, PPMd, and filters
|
|
29
|
+
- ๐ **Encrypted archives**: AES-256/AES-128 decryption for RAR4 & RAR5
|
|
30
|
+
- ๐ **RAR4 + RAR5**: Full support for both RAR formats
|
|
16
31
|
- ๐ **Browser support**: WASM build available
|
|
17
32
|
|
|
18
33
|
## Installation
|
|
19
34
|
|
|
35
|
+
### Rust
|
|
36
|
+
|
|
37
|
+
```toml
|
|
38
|
+
[dependencies]
|
|
39
|
+
rar-stream = { version = "5", features = ["async", "crypto"] }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Node.js
|
|
43
|
+
|
|
20
44
|
```bash
|
|
21
45
|
npm install rar-stream
|
|
22
46
|
# or
|
|
@@ -43,8 +67,34 @@ for (const file of files) {
|
|
|
43
67
|
// Read entire file into memory
|
|
44
68
|
const buffer = await file.readToEnd();
|
|
45
69
|
|
|
46
|
-
// Or
|
|
47
|
-
const
|
|
70
|
+
// Or stream (returns Node.js Readable)
|
|
71
|
+
const stream = file.createReadStream();
|
|
72
|
+
stream.pipe(process.stdout);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Rust Quick Start
|
|
77
|
+
|
|
78
|
+
```rust
|
|
79
|
+
use rar_stream::{RarFilesPackage, ParseOptions, LocalFileMedia, FileMedia};
|
|
80
|
+
use std::sync::Arc;
|
|
81
|
+
|
|
82
|
+
#[tokio::main]
|
|
83
|
+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
84
|
+
// Open a RAR archive
|
|
85
|
+
let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new("archive.rar")?);
|
|
86
|
+
let package = RarFilesPackage::new(vec![file]);
|
|
87
|
+
|
|
88
|
+
// Parse and list files
|
|
89
|
+
let files = package.parse(ParseOptions::default()).await?;
|
|
90
|
+
for f in &files {
|
|
91
|
+
println!("{}: {} bytes", f.name, f.length);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Read file content
|
|
95
|
+
let content = files[0].read_to_end().await?;
|
|
96
|
+
println!("Read {} bytes", content.len());
|
|
97
|
+
Ok(())
|
|
48
98
|
}
|
|
49
99
|
```
|
|
50
100
|
|
|
@@ -69,10 +119,11 @@ if (targetFile) {
|
|
|
69
119
|
}
|
|
70
120
|
```
|
|
71
121
|
|
|
72
|
-
### Stream Video from RAR (
|
|
122
|
+
### Stream Video from RAR (Node.js Readable Stream)
|
|
73
123
|
|
|
74
124
|
```javascript
|
|
75
125
|
import { LocalFileMedia, RarFilesPackage } from 'rar-stream';
|
|
126
|
+
import fs from 'fs';
|
|
76
127
|
|
|
77
128
|
const media = new LocalFileMedia('./movie.rar');
|
|
78
129
|
const pkg = new RarFilesPackage([media]);
|
|
@@ -80,20 +131,92 @@ const files = await pkg.parse();
|
|
|
80
131
|
|
|
81
132
|
const video = files.find(f => f.name.endsWith('.mkv'));
|
|
82
133
|
if (video) {
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
|
|
134
|
+
// Get a Node.js Readable stream for the entire file
|
|
135
|
+
const stream = video.createReadStream();
|
|
136
|
+
stream.pipe(fs.createWriteStream('./extracted-video.mkv'));
|
|
86
137
|
|
|
87
|
-
//
|
|
88
|
-
const
|
|
89
|
-
for (let offset = 0; offset < video.length; offset += chunkSize) {
|
|
90
|
-
const end = Math.min(offset + chunkSize - 1, video.length - 1);
|
|
91
|
-
const chunk = await video.createReadStream({ start: offset, end });
|
|
92
|
-
// Process chunk...
|
|
93
|
-
}
|
|
138
|
+
// Or stream a specific byte range (for HTTP range requests)
|
|
139
|
+
const rangeStream = video.createReadStream({ start: 0, end: 1024 * 1024 - 1 });
|
|
94
140
|
}
|
|
95
141
|
```
|
|
96
142
|
|
|
143
|
+
### WebTorrent Integration
|
|
144
|
+
|
|
145
|
+
Use `rar-stream` with WebTorrent to stream video from RAR archives inside torrents:
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
import WebTorrent from 'webtorrent';
|
|
149
|
+
import { RarFilesPackage } from 'rar-stream';
|
|
150
|
+
|
|
151
|
+
const client = new WebTorrent();
|
|
152
|
+
|
|
153
|
+
client.add(magnetUri, (torrent) => {
|
|
154
|
+
// Find RAR files (includes .rar, .r00, .r01, etc. for multi-volume)
|
|
155
|
+
// WebTorrent files already implement the FileMedia interface!
|
|
156
|
+
const rarFiles = torrent.files
|
|
157
|
+
.filter(f => /\.(rar|r\d{2})$/i.test(f.name))
|
|
158
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
159
|
+
|
|
160
|
+
// No wrapper needed - pass torrent files directly
|
|
161
|
+
const pkg = new RarFilesPackage(rarFiles);
|
|
162
|
+
pkg.parse().then(innerFiles => {
|
|
163
|
+
const video = innerFiles.find(f => f.name.endsWith('.mkv'));
|
|
164
|
+
if (video) {
|
|
165
|
+
// Stream video content - this returns a Node.js Readable
|
|
166
|
+
const stream = video.createReadStream();
|
|
167
|
+
|
|
168
|
+
// Pipe to HTTP response, media player, etc.
|
|
169
|
+
stream.pipe(process.stdout);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### HTTP Range Request Handler (Express)
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
import express from 'express';
|
|
179
|
+
import { LocalFileMedia, RarFilesPackage } from 'rar-stream';
|
|
180
|
+
|
|
181
|
+
const app = express();
|
|
182
|
+
|
|
183
|
+
// Pre-parse the RAR archive
|
|
184
|
+
const media = new LocalFileMedia('./videos.rar');
|
|
185
|
+
const pkg = new RarFilesPackage([media]);
|
|
186
|
+
const files = await pkg.parse();
|
|
187
|
+
const video = files.find(f => f.name.endsWith('.mp4'));
|
|
188
|
+
|
|
189
|
+
app.get('/video', (req, res) => {
|
|
190
|
+
const range = req.headers.range;
|
|
191
|
+
const fileSize = video.length;
|
|
192
|
+
|
|
193
|
+
if (range) {
|
|
194
|
+
const [startStr, endStr] = range.replace(/bytes=/, '').split('-');
|
|
195
|
+
const start = parseInt(startStr, 10);
|
|
196
|
+
const end = endStr ? parseInt(endStr, 10) : fileSize - 1;
|
|
197
|
+
|
|
198
|
+
res.writeHead(206, {
|
|
199
|
+
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
|
200
|
+
'Accept-Ranges': 'bytes',
|
|
201
|
+
'Content-Length': end - start + 1,
|
|
202
|
+
'Content-Type': 'video/mp4',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Stream the range directly from the RAR archive
|
|
206
|
+
const stream = video.createReadStream({ start, end });
|
|
207
|
+
stream.pipe(res);
|
|
208
|
+
} else {
|
|
209
|
+
res.writeHead(200, {
|
|
210
|
+
'Content-Length': fileSize,
|
|
211
|
+
'Content-Type': 'video/mp4',
|
|
212
|
+
});
|
|
213
|
+
video.createReadStream().pipe(res);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app.listen(3000);
|
|
218
|
+
```
|
|
219
|
+
|
|
97
220
|
### Multi-Volume Archives
|
|
98
221
|
|
|
99
222
|
```javascript
|
|
@@ -170,7 +293,21 @@ class LocalFileMedia {
|
|
|
170
293
|
readonly name: string; // Filename (basename)
|
|
171
294
|
readonly length: number; // File size in bytes
|
|
172
295
|
|
|
173
|
-
|
|
296
|
+
// Read a byte range into a Buffer
|
|
297
|
+
// Create a Readable stream for a byte range
|
|
298
|
+
createReadStream(opts: { start: number; end: number }): Readable;
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### FileMedia Interface
|
|
303
|
+
|
|
304
|
+
Custom data sources (WebTorrent, S3, HTTP, etc.) must implement this interface:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
interface FileMedia {
|
|
308
|
+
readonly name: string;
|
|
309
|
+
readonly length: number;
|
|
310
|
+
createReadStream(opts: { start: number; end: number }): Readable;
|
|
174
311
|
}
|
|
175
312
|
```
|
|
176
313
|
|
|
@@ -180,7 +317,7 @@ Parses single or multi-volume RAR archives.
|
|
|
180
317
|
|
|
181
318
|
```typescript
|
|
182
319
|
class RarFilesPackage {
|
|
183
|
-
constructor(files:
|
|
320
|
+
constructor(files: FileMedia[]); // LocalFileMedia or custom FileMedia
|
|
184
321
|
|
|
185
322
|
parse(opts?: {
|
|
186
323
|
maxFiles?: number; // Limit number of files to parse
|
|
@@ -193,12 +330,20 @@ class RarFilesPackage {
|
|
|
193
330
|
Represents a file inside the RAR archive.
|
|
194
331
|
|
|
195
332
|
```typescript
|
|
333
|
+
import { Readable } from 'stream';
|
|
334
|
+
|
|
196
335
|
class InnerFile {
|
|
197
336
|
readonly name: string; // Full path inside archive
|
|
198
337
|
readonly length: number; // Uncompressed size in bytes
|
|
199
338
|
|
|
339
|
+
// Read entire file into memory
|
|
200
340
|
readToEnd(): Promise<Buffer>;
|
|
201
|
-
|
|
341
|
+
|
|
342
|
+
// Create a Readable stream for the entire file or a byte range
|
|
343
|
+
createReadStream(opts?: {
|
|
344
|
+
start?: number; // Default: 0
|
|
345
|
+
end?: number; // Default: length - 1
|
|
346
|
+
}): Readable;
|
|
202
347
|
}
|
|
203
348
|
```
|
|
204
349
|
|
|
@@ -211,6 +356,12 @@ function isRarArchive(buffer: Buffer): boolean;
|
|
|
211
356
|
// Parse RAR header from buffer (needs ~300 bytes)
|
|
212
357
|
function parseRarHeader(buffer: Buffer): RarFileInfo | null;
|
|
213
358
|
|
|
359
|
+
// Convert a Readable stream to a Buffer
|
|
360
|
+
function streamToBuffer(stream: Readable): Promise<Buffer>;
|
|
361
|
+
|
|
362
|
+
// Create a FileMedia from any source with createReadStream
|
|
363
|
+
function createFileMedia(source: FileMedia): FileMedia;
|
|
364
|
+
|
|
214
365
|
interface RarFileInfo {
|
|
215
366
|
name: string;
|
|
216
367
|
packedSize: number;
|
|
@@ -222,22 +373,123 @@ interface RarFileInfo {
|
|
|
222
373
|
|
|
223
374
|
## Compression Support
|
|
224
375
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
|
228
|
-
|
|
229
|
-
|
|
|
230
|
-
|
|
|
376
|
+
### RAR Format Compatibility
|
|
377
|
+
|
|
378
|
+
| Format | Signature | Support |
|
|
379
|
+
|--------|-----------|---------|
|
|
380
|
+
| RAR 1.5-4.x (RAR4) | `Rar!\x1a\x07\x00` | โ
Full |
|
|
381
|
+
| RAR 5.0+ (RAR5) | `Rar!\x1a\x07\x01\x00` | โ
Full |
|
|
382
|
+
|
|
383
|
+
### Compression Methods
|
|
384
|
+
|
|
385
|
+
| Method | RAR4 | RAR5 | Description |
|
|
386
|
+
|--------|------|------|-------------|
|
|
387
|
+
| Store | โ
| โ
| No compression |
|
|
388
|
+
| LZSS | โ
| โ
| Huffman + LZ77 sliding window |
|
|
389
|
+
| PPMd | โ
| โ | Context-based (RAR4 only) |
|
|
390
|
+
|
|
391
|
+
### Filter Support
|
|
392
|
+
|
|
393
|
+
| Filter | RAR4 | RAR5 | Description |
|
|
394
|
+
|--------|------|------|-------------|
|
|
395
|
+
| E8 | โ
| โ
| x86 CALL preprocessing |
|
|
396
|
+
| E8E9 | โ
| โ
| x86 CALL/JMP preprocessing |
|
|
397
|
+
| Delta | โ
| โ
| Byte delta per channel |
|
|
398
|
+
| ARM | โ | โ
| ARM branch preprocessing |
|
|
399
|
+
| Itanium | โ
| โ | IA-64 preprocessing |
|
|
400
|
+
| RGB | โ
| โ | Predictive color filter |
|
|
401
|
+
| Audio | โ
| โ | Audio sample predictor |
|
|
402
|
+
|
|
403
|
+
### Encryption Support
|
|
404
|
+
|
|
405
|
+
| Feature | RAR4 | RAR5 | Notes |
|
|
406
|
+
|---------|------|------|-------|
|
|
407
|
+
| Encrypted files | โ
| โ
| `crypto` feature |
|
|
408
|
+
| Encrypted headers | โ | โ
| RAR5 `-hp` archives |
|
|
409
|
+
| Algorithm | AES-128-CBC | AES-256-CBC | โ |
|
|
410
|
+
| Key derivation | SHA-1 (262k rounds) | PBKDF2-HMAC-SHA256 | โ |
|
|
411
|
+
|
|
412
|
+
To enable encryption support:
|
|
413
|
+
|
|
414
|
+
**Node.js/npm:** Encryption is always available.
|
|
415
|
+
|
|
416
|
+
**Rust:**
|
|
417
|
+
```toml
|
|
418
|
+
[dependencies]
|
|
419
|
+
rar-stream = { version = "5", features = ["async", "crypto"] }
|
|
420
|
+
```
|
|
231
421
|
|
|
232
422
|
## Performance
|
|
233
423
|
|
|
234
|
-
|
|
424
|
+
### RAR5 Decompression: Faster Than unrar
|
|
425
|
+
|
|
426
|
+
rar-stream's parallel pipeline **beats the official C `unrar`** (v7.0, multi-threaded) across all tested workloads.
|
|
427
|
+
|
|
428
|
+
Benchmark on AMD Ryzen 5 7640HS (6 cores):
|
|
429
|
+
|
|
430
|
+
| Archive | Size | rar-stream (pipeline) | unrar | Ratio |
|
|
431
|
+
|---------|------|----------------------|-------|-------|
|
|
432
|
+
| ISO (binary) | 200 MB | **422ms** | 453ms | **0.93ร** |
|
|
433
|
+
| Text | 200 MB | **144ms** | 202ms | **0.71ร** |
|
|
434
|
+
| Mixed | 200 MB | **342ms** | 527ms | **0.65ร** |
|
|
435
|
+
| Binary | 500 MB | **824ms** | 1149ms | **0.72ร** |
|
|
436
|
+
| Text | 500 MB | **424ms** | 604ms | **0.70ร** |
|
|
437
|
+
| Mixed | 1 GB | **1953ms** | 2550ms | **0.77ร** |
|
|
438
|
+
|
|
439
|
+
**Wins 24 out of 24 benchmark scenarios** across 6 data types ร 4 compression settings.
|
|
440
|
+
|
|
441
|
+
Best case: **1.9ร faster** than unrar (ISO 200MB, `-m5 -md128m`).
|
|
442
|
+
|
|
443
|
+
<details>
|
|
444
|
+
<summary>Full benchmark matrix (24 scenarios)</summary>
|
|
445
|
+
|
|
446
|
+
```
|
|
447
|
+
Archive Single Pipeline Unrar Pipe/Unrar
|
|
448
|
+
----------------------------------------------------------------
|
|
449
|
+
bin-500_m3_32m 1278ms 884ms 1187ms 0.74x
|
|
450
|
+
bin-500_m5_128m 1200ms 824ms 1149ms 0.72x
|
|
451
|
+
bin-500_m5_32m 1247ms 852ms 1162ms 0.73x
|
|
452
|
+
bin-500_m5_4m 1378ms 942ms 1770ms 0.53x
|
|
453
|
+
iso-200_m3_32m 715ms 426ms 760ms 0.56x
|
|
454
|
+
iso-200_m5_128m 720ms 423ms 811ms 0.52x
|
|
455
|
+
iso-200_m5_32m 721ms 422ms 453ms 0.93x
|
|
456
|
+
iso-200_m5_4m 717ms 422ms 442ms 0.95x
|
|
457
|
+
mixed-1g_m3_32m 2974ms 2109ms 2775ms 0.76x
|
|
458
|
+
mixed-1g_m5_128m 3177ms 2213ms 2984ms 0.74x
|
|
459
|
+
mixed-1g_m5_32m 2979ms 2086ms 2731ms 0.76x
|
|
460
|
+
mixed-1g_m5_4m 2761ms 1953ms 2550ms 0.77x
|
|
461
|
+
mixed-200_m3_32m 499ms 385ms 547ms 0.70x
|
|
462
|
+
mixed-200_m5_128m 438ms 342ms 527ms 0.65x
|
|
463
|
+
mixed-200_m5_32m 495ms 384ms 539ms 0.71x
|
|
464
|
+
mixed-200_m5_4m 511ms 395ms 538ms 0.73x
|
|
465
|
+
text-200_m3_32m 209ms 145ms 202ms 0.72x
|
|
466
|
+
text-200_m5_128m 205ms 144ms 239ms 0.60x
|
|
467
|
+
text-200_m5_32m 209ms 144ms 202ms 0.71x
|
|
468
|
+
text-200_m5_4m 227ms 153ms 207ms 0.74x
|
|
469
|
+
text-500_m3_32m 606ms 432ms 613ms 0.70x
|
|
470
|
+
text-500_m5_128m 601ms 431ms 644ms 0.67x
|
|
471
|
+
text-500_m5_32m 604ms 424ms 604ms 0.70x
|
|
472
|
+
text-500_m5_4m 659ms 455ms 643ms 0.71x
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
</details>
|
|
476
|
+
|
|
477
|
+
### Node.js (NAPI) Performance
|
|
478
|
+
|
|
479
|
+
| Configuration | Time (200MB) | vs unrar |
|
|
480
|
+
|---------------|-------------|----------|
|
|
481
|
+
| v5.0.0 (single-threaded) | 1127ms | 2.73ร slower |
|
|
482
|
+
| **v5.1.0 (pipeline)** | **673ms** | **1.53ร slower** |
|
|
483
|
+
|
|
484
|
+
The remaining NAPI gap vs native is I/O + buffer copy overhead, not decompression.
|
|
485
|
+
|
|
486
|
+
### Optimization Features
|
|
235
487
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
488
|
+
- **Parallel pipeline**: Decode + apply in parallel using rayon worker threads
|
|
489
|
+
- **Split-buffer decode**: Separates literals from commands for cache-friendly apply
|
|
490
|
+
- **LTO (Link-Time Optimization)**: Enabled by default in release builds
|
|
491
|
+
- **SIMD**: Automatic vectorization for E8/E9 filter scanning and memcpy
|
|
492
|
+
- **Zero-copy streaming**: Direct buffer access without intermediate copies
|
|
241
493
|
|
|
242
494
|
## Migrating from v3.x
|
|
243
495
|
|
package/lib/browser.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rar-stream browser entry point with Web Streams API support
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Re-export WASM bindings
|
|
6
|
+
export {
|
|
7
|
+
default as init,
|
|
8
|
+
initSync,
|
|
9
|
+
isRarArchive,
|
|
10
|
+
getRarVersion,
|
|
11
|
+
parseRarHeader,
|
|
12
|
+
RarDecoder,
|
|
13
|
+
WasmRar5Crypto,
|
|
14
|
+
is_rar_archive,
|
|
15
|
+
get_rar_version,
|
|
16
|
+
parse_rar_header,
|
|
17
|
+
WasmRarDecoder,
|
|
18
|
+
} from '../browser.js';
|
|
19
|
+
|
|
20
|
+
/** Options for creating a ReadableStream */
|
|
21
|
+
export interface ReadableStreamOptions {
|
|
22
|
+
/** Total size of the data */
|
|
23
|
+
totalSize: number;
|
|
24
|
+
/** Start offset (default: 0) */
|
|
25
|
+
start?: number;
|
|
26
|
+
/** End offset inclusive (default: totalSize - 1) */
|
|
27
|
+
end?: number;
|
|
28
|
+
/** Size of each chunk to read (default: 65536) */
|
|
29
|
+
chunkSize?: number;
|
|
30
|
+
/** Function to read a chunk of data */
|
|
31
|
+
readChunk: (start: number, end: number) => Promise<Uint8Array>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Options for creating a range response */
|
|
35
|
+
export interface RangeResponseOptions {
|
|
36
|
+
/** Total file size */
|
|
37
|
+
totalSize: number;
|
|
38
|
+
/** HTTP Range header value */
|
|
39
|
+
rangeHeader?: string;
|
|
40
|
+
/** MIME type (default: 'application/octet-stream') */
|
|
41
|
+
contentType?: string;
|
|
42
|
+
/** Function to read a chunk of data */
|
|
43
|
+
readChunk: (start: number, end: number) => Promise<Uint8Array>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Result from createRangeResponse */
|
|
47
|
+
export interface RangeResponseResult {
|
|
48
|
+
stream: ReadableStream<Uint8Array>;
|
|
49
|
+
headers: Headers;
|
|
50
|
+
status: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a Web ReadableStream from an async data source.
|
|
55
|
+
* Useful for Service Workers and streaming responses.
|
|
56
|
+
*/
|
|
57
|
+
export declare function createReadableStream(options: ReadableStreamOptions): ReadableStream<Uint8Array>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Helper to create a streaming response for HTTP range requests.
|
|
61
|
+
* Parses the Range header and returns appropriate stream, headers, and status.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createRangeResponse(options: RangeResponseOptions): RangeResponseResult;
|
package/lib/browser.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rar-stream browser entry point with Web Streams API support
|
|
3
|
+
*
|
|
4
|
+
* Provides ReadableStream support for streaming file content in browsers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Re-export all WASM bindings
|
|
8
|
+
export {
|
|
9
|
+
default as init,
|
|
10
|
+
initSync,
|
|
11
|
+
is_rar_archive as isRarArchive,
|
|
12
|
+
get_rar_version as getRarVersion,
|
|
13
|
+
parse_rar_header as parseRarHeader,
|
|
14
|
+
WasmRarDecoder as RarDecoder,
|
|
15
|
+
} from '../pkg/rar_stream.js';
|
|
16
|
+
|
|
17
|
+
// Also export snake_case versions for compatibility
|
|
18
|
+
export {
|
|
19
|
+
is_rar_archive,
|
|
20
|
+
get_rar_version,
|
|
21
|
+
parse_rar_header,
|
|
22
|
+
WasmRarDecoder,
|
|
23
|
+
} from '../pkg/rar_stream.js';
|
|
24
|
+
|
|
25
|
+
// Re-export crypto if available
|
|
26
|
+
export { WasmRar5Crypto } from '../pkg/rar_stream.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a Web ReadableStream from an async data source.
|
|
30
|
+
*
|
|
31
|
+
* This utility helps create streaming responses for browsers,
|
|
32
|
+
* useful for Service Workers and fetch handlers.
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} options
|
|
35
|
+
* @param {number} options.totalSize - Total size of the data
|
|
36
|
+
* @param {number} [options.start=0] - Start offset
|
|
37
|
+
* @param {number} [options.end] - End offset (inclusive), defaults to totalSize-1
|
|
38
|
+
* @param {number} [options.chunkSize=65536] - Size of each chunk to read
|
|
39
|
+
* @param {function(start: number, end: number): Promise<Uint8Array>} options.readChunk - Function to read a chunk
|
|
40
|
+
* @returns {ReadableStream<Uint8Array>}
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // In a Service Worker fetch handler
|
|
44
|
+
* const stream = createReadableStream({
|
|
45
|
+
* totalSize: file.length,
|
|
46
|
+
* start: rangeStart,
|
|
47
|
+
* end: rangeEnd,
|
|
48
|
+
* readChunk: async (start, end) => {
|
|
49
|
+
* const decoder = new WasmRarDecoder(file.unpackedSize);
|
|
50
|
+
* // ... fetch and decompress data
|
|
51
|
+
* return decompressedData.slice(start, end + 1);
|
|
52
|
+
* }
|
|
53
|
+
* });
|
|
54
|
+
* return new Response(stream, { headers: { 'Content-Type': 'video/mp4' } });
|
|
55
|
+
*/
|
|
56
|
+
export function createReadableStream(options) {
|
|
57
|
+
const { totalSize, start = 0, end = totalSize - 1, chunkSize = 64 * 1024, readChunk } = options;
|
|
58
|
+
let offset = start;
|
|
59
|
+
|
|
60
|
+
return new ReadableStream({
|
|
61
|
+
async pull(controller) {
|
|
62
|
+
if (offset > end) {
|
|
63
|
+
controller.close();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const chunkEnd = Math.min(offset + chunkSize - 1, end);
|
|
68
|
+
try {
|
|
69
|
+
const chunk = await readChunk(offset, chunkEnd);
|
|
70
|
+
controller.enqueue(chunk);
|
|
71
|
+
offset = chunkEnd + 1;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
controller.error(err);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Helper to create a streaming response for range requests.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} options
|
|
83
|
+
* @param {number} options.totalSize - Total file size
|
|
84
|
+
* @param {string} [options.rangeHeader] - HTTP Range header value
|
|
85
|
+
* @param {string} [options.contentType='application/octet-stream'] - MIME type
|
|
86
|
+
* @param {function(start: number, end: number): Promise<Uint8Array>} options.readChunk
|
|
87
|
+
* @returns {{ stream: ReadableStream, headers: Headers, status: number }}
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* // In a Service Worker
|
|
91
|
+
* const { stream, headers, status } = createRangeResponse({
|
|
92
|
+
* totalSize: innerFile.length,
|
|
93
|
+
* rangeHeader: request.headers.get('Range'),
|
|
94
|
+
* contentType: 'video/mp4',
|
|
95
|
+
* readChunk: async (start, end) => decompress(start, end),
|
|
96
|
+
* });
|
|
97
|
+
* return new Response(stream, { status, headers });
|
|
98
|
+
*/
|
|
99
|
+
export function createRangeResponse(options) {
|
|
100
|
+
const { totalSize, rangeHeader, contentType = 'application/octet-stream', readChunk } = options;
|
|
101
|
+
|
|
102
|
+
let start = 0;
|
|
103
|
+
let end = totalSize - 1;
|
|
104
|
+
let status = 200;
|
|
105
|
+
|
|
106
|
+
const headers = new Headers({
|
|
107
|
+
'Content-Type': contentType,
|
|
108
|
+
'Accept-Ranges': 'bytes',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Parse Range header if present
|
|
112
|
+
if (rangeHeader) {
|
|
113
|
+
const match = rangeHeader.match(/bytes=(\d*)-(\d*)/);
|
|
114
|
+
if (match) {
|
|
115
|
+
start = match[1] ? parseInt(match[1], 10) : 0;
|
|
116
|
+
end = match[2] ? parseInt(match[2], 10) : totalSize - 1;
|
|
117
|
+
status = 206;
|
|
118
|
+
headers.set('Content-Range', `bytes ${start}-${end}/${totalSize}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
headers.set('Content-Length', String(end - start + 1));
|
|
123
|
+
|
|
124
|
+
const stream = createReadableStream({
|
|
125
|
+
totalSize,
|
|
126
|
+
start,
|
|
127
|
+
end,
|
|
128
|
+
readChunk,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return { stream, headers, status };
|
|
132
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rar-stream - Node.js wrapper with Readable stream support
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Readable } from 'stream';
|
|
6
|
+
|
|
7
|
+
/** Read interval options. */
|
|
8
|
+
export interface ReadIntervalJs {
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Stream options for createReadStream. */
|
|
14
|
+
export interface StreamOptions {
|
|
15
|
+
start?: number;
|
|
16
|
+
end?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Parse options for filtering results. */
|
|
20
|
+
export interface ParseOptionsJs {
|
|
21
|
+
maxFiles?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Parsed file info from RAR header. */
|
|
25
|
+
export interface RarFileInfo {
|
|
26
|
+
name: string;
|
|
27
|
+
packedSize: number;
|
|
28
|
+
unpackedSize: number;
|
|
29
|
+
method: number;
|
|
30
|
+
continuesInNext: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* FileMedia interface for custom file sources.
|
|
35
|
+
* Implement this to use WebTorrent, HTTP, S3, etc.
|
|
36
|
+
*/
|
|
37
|
+
export interface FileMedia {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly length: number;
|
|
40
|
+
createReadStream(opts: ReadIntervalJs): Readable;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Helper to read a Readable stream into a Buffer.
|
|
45
|
+
*/
|
|
46
|
+
export declare function streamToBuffer(stream: Readable): Promise<Buffer>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a FileMedia wrapper from any object with createReadStream.
|
|
50
|
+
* Use this to wrap WebTorrent files, HTTP responses, S3 objects, etc.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createFileMedia(source: FileMedia): FileMedia;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse RAR file header from a buffer.
|
|
56
|
+
* This can be used to detect RAR archives and get inner file info
|
|
57
|
+
* without downloading the entire archive.
|
|
58
|
+
*
|
|
59
|
+
* The buffer should contain at least the first ~300 bytes of a .rar file.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseRarHeader(buffer: Buffer): RarFileInfo | null;
|
|
62
|
+
|
|
63
|
+
/** Check if a buffer starts with a RAR signature. */
|
|
64
|
+
export declare function isRarArchive(buffer: Buffer): boolean;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* LocalFileMedia - reads from local filesystem.
|
|
68
|
+
* Implements the FileMedia interface.
|
|
69
|
+
*/
|
|
70
|
+
export declare class LocalFileMedia implements FileMedia {
|
|
71
|
+
constructor(path: string);
|
|
72
|
+
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly length: number;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a Readable stream for a byte range.
|
|
78
|
+
* @param opts Byte range (inclusive)
|
|
79
|
+
*/
|
|
80
|
+
createReadStream(opts: ReadIntervalJs): Readable;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* InnerFile - a file inside the RAR archive.
|
|
85
|
+
* Provides stream-based access to file content.
|
|
86
|
+
*/
|
|
87
|
+
export declare class InnerFile {
|
|
88
|
+
readonly name: string;
|
|
89
|
+
readonly length: number;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a Readable stream for the entire file or a byte range.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* // Stream entire file
|
|
96
|
+
* const stream = file.createReadStream();
|
|
97
|
+
* stream.pipe(fs.createWriteStream('output.bin'));
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* // Stream with range (for HTTP range requests, WebTorrent, etc.)
|
|
101
|
+
* const stream = file.createReadStream({ start: 0, end: 1024 * 1024 - 1 });
|
|
102
|
+
*/
|
|
103
|
+
createReadStream(opts?: StreamOptions): Readable;
|
|
104
|
+
|
|
105
|
+
/** Read entire file into memory. */
|
|
106
|
+
readToEnd(): Promise<Buffer>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* RarFilesPackage - parses multi-volume RAR archives.
|
|
111
|
+
*
|
|
112
|
+
* Supports both LocalFileMedia and custom FileMedia implementations.
|
|
113
|
+
*/
|
|
114
|
+
export declare class RarFilesPackage {
|
|
115
|
+
constructor(files: FileMedia[]);
|
|
116
|
+
|
|
117
|
+
/** Parse the archive and return inner files. */
|
|
118
|
+
parse(opts?: ParseOptionsJs | undefined | null): Promise<InnerFile[]>;
|
|
119
|
+
}
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rar-stream - Node.js wrapper with Readable stream support
|
|
3
|
+
*
|
|
4
|
+
* This module wraps the native NAPI bindings to provide Node.js Readable streams
|
|
5
|
+
* for streaming file content from RAR archives.
|
|
6
|
+
*
|
|
7
|
+
* Supports both native LocalFileMedia and custom FileMedia implementations
|
|
8
|
+
* (like WebTorrent files).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Readable } from 'stream';
|
|
12
|
+
import { createRequire } from 'module';
|
|
13
|
+
|
|
14
|
+
// Import native bindings (CommonJS, auto-generated by NAPI-RS)
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const native = require('../index.js');
|
|
17
|
+
const {
|
|
18
|
+
LocalFileMedia: NativeLocalFileMedia,
|
|
19
|
+
InnerFile: NativeInnerFile,
|
|
20
|
+
RarFilesPackage: NativeRarFilesPackage,
|
|
21
|
+
parseRarHeader,
|
|
22
|
+
isRarArchive,
|
|
23
|
+
} = native;
|
|
24
|
+
|
|
25
|
+
// Re-export utility functions
|
|
26
|
+
export { parseRarHeader, isRarArchive };
|
|
27
|
+
|
|
28
|
+
// RAR signatures
|
|
29
|
+
const RAR4_SIGNATURE = Buffer.from([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]);
|
|
30
|
+
const RAR5_SIGNATURE = Buffer.from([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Helper to read a Readable stream into a Buffer.
|
|
34
|
+
* @param {Readable} stream
|
|
35
|
+
* @returns {Promise<Buffer>}
|
|
36
|
+
*/
|
|
37
|
+
export function streamToBuffer(stream) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
stream.on('data', chunk => chunks.push(chunk));
|
|
41
|
+
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
42
|
+
stream.on('error', reject);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a file is a native LocalFileMedia
|
|
48
|
+
*/
|
|
49
|
+
function isNativeMedia(file) {
|
|
50
|
+
return file._native instanceof NativeLocalFileMedia ||
|
|
51
|
+
file instanceof NativeLocalFileMedia ||
|
|
52
|
+
(file._nativeMedia && file._nativeMedia instanceof NativeLocalFileMedia);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a FileMedia wrapper from any object with createReadStream.
|
|
57
|
+
*
|
|
58
|
+
* This is optional - any object with name, length, and createReadStream
|
|
59
|
+
* can be passed directly to RarFilesPackage (like WebTorrent files).
|
|
60
|
+
*
|
|
61
|
+
* Use this helper when your source has different property names or
|
|
62
|
+
* needs adaptation to the FileMedia interface.
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} source - Object implementing FileMedia interface
|
|
65
|
+
* @returns {FileMedia}
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Adapt an S3 object to FileMedia
|
|
69
|
+
* const media = createFileMedia({
|
|
70
|
+
* name: s3Object.Key,
|
|
71
|
+
* length: s3Object.ContentLength,
|
|
72
|
+
* createReadStream: ({ start, end }) => s3.getObject({
|
|
73
|
+
* Bucket: bucket,
|
|
74
|
+
* Key: s3Object.Key,
|
|
75
|
+
* Range: `bytes=${start}-${end}`,
|
|
76
|
+
* }).createReadStream(),
|
|
77
|
+
* });
|
|
78
|
+
*/
|
|
79
|
+
export function createFileMedia(source) {
|
|
80
|
+
return {
|
|
81
|
+
get name() { return source.name; },
|
|
82
|
+
get length() { return source.length; },
|
|
83
|
+
createReadStream(opts) {
|
|
84
|
+
return source.createReadStream(opts);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* LocalFileMedia - reads from local filesystem.
|
|
91
|
+
* Implements the FileMedia interface.
|
|
92
|
+
*/
|
|
93
|
+
export class LocalFileMedia {
|
|
94
|
+
constructor(path) {
|
|
95
|
+
this._native = new NativeLocalFileMedia(path);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get name() {
|
|
99
|
+
return this._native.name;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get length() {
|
|
103
|
+
return this._native.length;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a Readable stream for a byte range.
|
|
108
|
+
* @param {{ start: number, end: number }} opts - Byte range (inclusive)
|
|
109
|
+
* @returns {Readable}
|
|
110
|
+
*/
|
|
111
|
+
createReadStream(opts) {
|
|
112
|
+
const { start, end } = opts;
|
|
113
|
+
const media = this._native;
|
|
114
|
+
let offset = start;
|
|
115
|
+
|
|
116
|
+
return new Readable({
|
|
117
|
+
highWaterMark: 64 * 1024,
|
|
118
|
+
async read(size) {
|
|
119
|
+
if (offset > end) {
|
|
120
|
+
this.push(null);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const chunkEnd = Math.min(offset + size - 1, end);
|
|
124
|
+
try {
|
|
125
|
+
// Native returns Promise<Buffer>, we stream it
|
|
126
|
+
const chunk = await media.createReadStream({ start: offset, end: chunkEnd });
|
|
127
|
+
offset = chunkEnd + 1;
|
|
128
|
+
this.push(chunk);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
this.destroy(err);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @internal */
|
|
137
|
+
get _nativeMedia() {
|
|
138
|
+
return this._native;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* InnerFile - a file inside the RAR archive.
|
|
144
|
+
* Wraps the native implementation with stream support.
|
|
145
|
+
*/
|
|
146
|
+
export class InnerFile {
|
|
147
|
+
constructor(nativeInnerFile) {
|
|
148
|
+
this._native = nativeInnerFile;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get name() {
|
|
152
|
+
return this._native.name;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get length() {
|
|
156
|
+
return this._native.length;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create a Readable stream for a byte range or entire file.
|
|
161
|
+
* @param {{ start?: number, end?: number }} [opts] - Byte range (inclusive)
|
|
162
|
+
* @returns {Readable}
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* // Stream entire file
|
|
166
|
+
* const stream = file.createReadStream();
|
|
167
|
+
* stream.pipe(fs.createWriteStream('output.bin'));
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* // Stream a specific range (for HTTP range requests)
|
|
171
|
+
* const stream = file.createReadStream({ start: 0, end: 1024 * 1024 - 1 });
|
|
172
|
+
* stream.pipe(res);
|
|
173
|
+
*/
|
|
174
|
+
createReadStream(opts = {}) {
|
|
175
|
+
const start = opts.start ?? 0;
|
|
176
|
+
const end = opts.end ?? this.length - 1;
|
|
177
|
+
const innerFile = this._native;
|
|
178
|
+
let offset = start;
|
|
179
|
+
|
|
180
|
+
return new Readable({
|
|
181
|
+
highWaterMark: 64 * 1024,
|
|
182
|
+
async read(size) {
|
|
183
|
+
if (offset > end) {
|
|
184
|
+
this.push(null);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const chunkEnd = Math.min(offset + size - 1, end);
|
|
188
|
+
try {
|
|
189
|
+
// Native returns Promise<Buffer>
|
|
190
|
+
const chunk = await innerFile.createReadStream({ start: offset, end: chunkEnd });
|
|
191
|
+
offset = chunkEnd + 1;
|
|
192
|
+
this.push(chunk);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
this.destroy(err);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Read entire file into memory.
|
|
202
|
+
* @returns {Promise<Buffer>}
|
|
203
|
+
*/
|
|
204
|
+
async readToEnd() {
|
|
205
|
+
return this._native.readToEnd();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* JsInnerFile - a file inside the RAR archive (JS-based implementation).
|
|
211
|
+
* Used when parsing with custom FileMedia implementations.
|
|
212
|
+
*/
|
|
213
|
+
class JsInnerFile {
|
|
214
|
+
constructor(opts) {
|
|
215
|
+
this._name = opts.name;
|
|
216
|
+
this._length = opts.unpackedSize;
|
|
217
|
+
this._packedSize = opts.packedSize;
|
|
218
|
+
this._method = opts.method;
|
|
219
|
+
this._dataOffset = opts.dataOffset;
|
|
220
|
+
this._media = opts.media;
|
|
221
|
+
this._isStored = opts.method === 0x30;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
get name() {
|
|
225
|
+
return this._name;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
get length() {
|
|
229
|
+
return this._length;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Create a Readable stream for a byte range or entire file.
|
|
234
|
+
* Note: Range reads only work for stored files (method 0x30).
|
|
235
|
+
* Compressed files require full decompression.
|
|
236
|
+
*
|
|
237
|
+
* @param {{ start?: number, end?: number }} [opts]
|
|
238
|
+
* @returns {Readable}
|
|
239
|
+
*/
|
|
240
|
+
createReadStream(opts = {}) {
|
|
241
|
+
const start = opts.start ?? 0;
|
|
242
|
+
const end = opts.end ?? this.length - 1;
|
|
243
|
+
const file = this;
|
|
244
|
+
|
|
245
|
+
if (!this._isStored) {
|
|
246
|
+
// For compressed files, throw an error for range reads
|
|
247
|
+
if (start !== 0 || end !== this.length - 1) {
|
|
248
|
+
throw new Error('Range reads not supported for compressed files. Use readToEnd() instead.');
|
|
249
|
+
}
|
|
250
|
+
// TODO: streaming decompression
|
|
251
|
+
throw new Error('Streaming compressed files not yet supported for custom FileMedia.');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// For stored files, stream directly from the source
|
|
255
|
+
const dataOffset = this._dataOffset;
|
|
256
|
+
const media = this._media;
|
|
257
|
+
let offset = start;
|
|
258
|
+
|
|
259
|
+
return new Readable({
|
|
260
|
+
highWaterMark: 64 * 1024,
|
|
261
|
+
async read(size) {
|
|
262
|
+
if (offset > end) {
|
|
263
|
+
this.push(null);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const chunkEnd = Math.min(offset + size - 1, end);
|
|
267
|
+
try {
|
|
268
|
+
// Get stream from underlying media and collect it
|
|
269
|
+
const dataStart = dataOffset + offset;
|
|
270
|
+
const dataEnd = dataOffset + chunkEnd;
|
|
271
|
+
const sourceStream = media.createReadStream({ start: dataStart, end: dataEnd });
|
|
272
|
+
const chunk = await streamToBuffer(sourceStream);
|
|
273
|
+
offset = chunkEnd + 1;
|
|
274
|
+
this.push(chunk);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
this.destroy(err);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Read entire file into memory.
|
|
284
|
+
* @returns {Promise<Buffer>}
|
|
285
|
+
*/
|
|
286
|
+
async readToEnd() {
|
|
287
|
+
const stream = this.createReadStream();
|
|
288
|
+
return streamToBuffer(stream);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* RarFilesPackage - parses multi-volume RAR archives.
|
|
294
|
+
*
|
|
295
|
+
* Supports both native LocalFileMedia and custom FileMedia implementations
|
|
296
|
+
* (like WebTorrent files). Custom implementations must have:
|
|
297
|
+
* - name: string
|
|
298
|
+
* - length: number
|
|
299
|
+
* - createReadStream({ start, end }): Readable
|
|
300
|
+
*/
|
|
301
|
+
export class RarFilesPackage {
|
|
302
|
+
constructor(files) {
|
|
303
|
+
this._files = files;
|
|
304
|
+
this._useNative = files.every(f => isNativeMedia(f));
|
|
305
|
+
|
|
306
|
+
if (this._useNative) {
|
|
307
|
+
const nativeFiles = files.map(f => f._nativeMedia ?? f._native ?? f);
|
|
308
|
+
this._native = new NativeRarFilesPackage(nativeFiles);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Parse the archive and return inner files.
|
|
314
|
+
* @param {{ maxFiles?: number }} [opts]
|
|
315
|
+
* @returns {Promise<InnerFile[]>}
|
|
316
|
+
*/
|
|
317
|
+
async parse(opts) {
|
|
318
|
+
if (this._useNative) {
|
|
319
|
+
const nativeFiles = await this._native.parse(opts);
|
|
320
|
+
return nativeFiles.map(f => new InnerFile(f));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// JS-based parsing for custom FileMedia
|
|
324
|
+
return this._parseJs(opts);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* JS-based parsing for custom FileMedia implementations.
|
|
329
|
+
* @private
|
|
330
|
+
*/
|
|
331
|
+
async _parseJs(opts) {
|
|
332
|
+
const maxFiles = opts?.maxFiles;
|
|
333
|
+
const results = [];
|
|
334
|
+
|
|
335
|
+
for (const media of this._files) {
|
|
336
|
+
// Read header data (first 512 bytes should be enough for header)
|
|
337
|
+
const headerSize = Math.min(512, media.length);
|
|
338
|
+
const headerStream = media.createReadStream({ start: 0, end: headerSize - 1 });
|
|
339
|
+
const headerData = await streamToBuffer(headerStream);
|
|
340
|
+
|
|
341
|
+
// Check signature
|
|
342
|
+
if (!isRarArchive(headerData)) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Determine RAR version
|
|
347
|
+
const isRar5 = headerData.slice(0, 8).equals(RAR5_SIGNATURE);
|
|
348
|
+
|
|
349
|
+
// Parse header to get file info
|
|
350
|
+
const header = parseRarHeader(headerData);
|
|
351
|
+
if (!header) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Calculate data offset (approximate - after headers)
|
|
356
|
+
// For RAR4: marker(7) + archive(13) + file header (variable)
|
|
357
|
+
// For RAR5: signature(8) + headers (variable length)
|
|
358
|
+
const dataOffset = media.length - header.packedSize;
|
|
359
|
+
|
|
360
|
+
results.push(new JsInnerFile({
|
|
361
|
+
name: header.name,
|
|
362
|
+
packedSize: header.packedSize,
|
|
363
|
+
unpackedSize: header.unpackedSize,
|
|
364
|
+
method: header.method,
|
|
365
|
+
dataOffset,
|
|
366
|
+
media,
|
|
367
|
+
}));
|
|
368
|
+
|
|
369
|
+
if (maxFiles && results.length >= maxFiles) {
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return results;
|
|
375
|
+
}
|
|
376
|
+
}
|
package/package.json
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rar-stream",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "RAR streaming library - Rust implementation with NAPI and WASM bindings",
|
|
5
|
-
"main": "index.
|
|
6
|
-
"browser": "browser.
|
|
7
|
-
"types": "index.d.ts",
|
|
5
|
+
"main": "lib/index.mjs",
|
|
6
|
+
"browser": "lib/browser.mjs",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"node": {
|
|
11
|
-
"types": "./index.d.ts",
|
|
12
|
-
"
|
|
11
|
+
"types": "./lib/index.d.ts",
|
|
12
|
+
"import": "./lib/index.mjs",
|
|
13
|
+
"require": "./index.js"
|
|
13
14
|
},
|
|
14
15
|
"browser": {
|
|
15
|
-
"types": "./browser.d.ts",
|
|
16
|
-
"
|
|
16
|
+
"types": "./lib/browser.d.ts",
|
|
17
|
+
"import": "./lib/browser.mjs"
|
|
17
18
|
},
|
|
18
|
-
"
|
|
19
|
+
"import": "./lib/index.mjs",
|
|
20
|
+
"require": "./index.js"
|
|
21
|
+
},
|
|
22
|
+
"./native": {
|
|
23
|
+
"types": "./index.d.ts",
|
|
24
|
+
"require": "./index.js"
|
|
19
25
|
}
|
|
20
26
|
},
|
|
21
27
|
"author": "doom-fish",
|
|
@@ -55,15 +61,20 @@
|
|
|
55
61
|
"scripts": {
|
|
56
62
|
"build": "napi build --platform --release --features napi",
|
|
57
63
|
"build:debug": "napi build --platform --features napi",
|
|
58
|
-
"build:wasm": "wasm-pack build --target web --features wasm --no-default-features",
|
|
64
|
+
"build:wasm": "wasm-pack build --target web --features wasm,crypto --no-default-features",
|
|
59
65
|
"lint": "eslint .",
|
|
60
|
-
"lint:rust": "cargo clippy --features napi -- -D warnings && cargo clippy --features wasm --no-default-features -- -D warnings",
|
|
66
|
+
"lint:rust": "cargo clippy --features napi -- -D warnings && cargo clippy --features wasm,crypto --no-default-features -- -D warnings",
|
|
61
67
|
"lint:fix": "eslint . --fix",
|
|
62
68
|
"format": "cargo fmt && prettier --write '**/*.{ts,js,json,md}'",
|
|
63
69
|
"format:check": "cargo fmt --check && prettier --check '**/*.{ts,js,json,md}'",
|
|
64
70
|
"test": "vitest run",
|
|
65
71
|
"test:watch": "vitest",
|
|
66
72
|
"test:browser": "npx playwright test",
|
|
73
|
+
"bench": "cargo bench",
|
|
74
|
+
"bench:save": "cargo bench -- --save-baseline main",
|
|
75
|
+
"bench:compare": "cargo bench -- --baseline main",
|
|
76
|
+
"profile": "cargo build --release && perf record -g ./target/release/examples/profile_decompress && perf report",
|
|
77
|
+
"flamegraph": "cargo flamegraph --bench decompress -- --bench",
|
|
67
78
|
"prepublishOnly": "napi prepublish -t npm --skip-gh-release"
|
|
68
79
|
},
|
|
69
80
|
"devDependencies": {
|
|
@@ -75,9 +86,14 @@
|
|
|
75
86
|
"prettier": "^3.2.0",
|
|
76
87
|
"typescript": "^5.4.0",
|
|
77
88
|
"typescript-eslint": "^8.0.0",
|
|
78
|
-
"vitest": "^2.1.8"
|
|
89
|
+
"vitest": "^2.1.8",
|
|
90
|
+
"webtorrent": "^2.8.5"
|
|
79
91
|
},
|
|
80
92
|
"files": [
|
|
93
|
+
"lib/index.mjs",
|
|
94
|
+
"lib/index.d.ts",
|
|
95
|
+
"lib/browser.mjs",
|
|
96
|
+
"lib/browser.d.ts",
|
|
81
97
|
"index.js",
|
|
82
98
|
"index.d.ts",
|
|
83
99
|
"browser.js",
|
|
@@ -89,12 +105,12 @@
|
|
|
89
105
|
"*.node"
|
|
90
106
|
],
|
|
91
107
|
"optionalDependencies": {
|
|
92
|
-
"rar-stream-win32-x64-msvc": "
|
|
93
|
-
"rar-stream-darwin-x64": "
|
|
94
|
-
"rar-stream-linux-x64-gnu": "
|
|
95
|
-
"rar-stream-linux-x64-musl": "
|
|
96
|
-
"rar-stream-linux-arm64-gnu": "
|
|
97
|
-
"rar-stream-darwin-arm64": "
|
|
98
|
-
"rar-stream-linux-arm64-musl": "
|
|
108
|
+
"rar-stream-win32-x64-msvc": "5.1.0",
|
|
109
|
+
"rar-stream-darwin-x64": "5.1.0",
|
|
110
|
+
"rar-stream-linux-x64-gnu": "5.1.0",
|
|
111
|
+
"rar-stream-linux-x64-musl": "5.1.0",
|
|
112
|
+
"rar-stream-linux-arm64-gnu": "5.1.0",
|
|
113
|
+
"rar-stream-darwin-arm64": "5.1.0",
|
|
114
|
+
"rar-stream-linux-arm64-musl": "5.1.0"
|
|
99
115
|
}
|
|
100
116
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|