rar-stream 5.7.0 → 5.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/browser.mjs DELETED
@@ -1,214 +0,0 @@
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
- import wasmInit, {
8
- initSync as _initSync,
9
- is_rar_archive,
10
- get_rar_version,
11
- parse_rar_header,
12
- parse_rar_headers,
13
- parse_rar5_header,
14
- parse_rar5_headers,
15
- extract_file,
16
- WasmRarArchive,
17
- WasmRarDecoder,
18
- WasmRar5Decoder,
19
- WasmRar5Crypto,
20
- } from '../pkg/rar_stream.js';
21
-
22
- let _initialized = false;
23
- let _initPromise = null;
24
-
25
- /**
26
- * Initialize the WASM module. Safe to call multiple times — subsequent calls are no-ops.
27
- * Automatically called by all exported functions, but can be called explicitly for eager loading.
28
- * @param {string | URL | Request | Response | BufferSource | WebAssembly.Module} [wasmUrl] - Optional WASM source
29
- */
30
- export async function init(wasmUrl) {
31
- if (_initialized) return;
32
- if (_initPromise) return _initPromise;
33
- _initPromise = wasmInit(wasmUrl).then(() => { _initialized = true; });
34
- return _initPromise;
35
- }
36
-
37
- export { _initSync as initSync };
38
-
39
- // Auto-init wrappers for all functions
40
- async function ensureInit() {
41
- if (!_initialized) await init();
42
- }
43
-
44
- /** Check if a buffer contains a RAR signature. */
45
- export async function isRarArchive(data) {
46
- await ensureInit();
47
- return is_rar_archive(data);
48
- }
49
-
50
- /** Get the RAR format version (15 for RAR4, 50 for RAR5, 0 if not RAR). */
51
- export async function getRarVersion(data) {
52
- await ensureInit();
53
- return get_rar_version(data);
54
- }
55
-
56
- /** Parse the first RAR4 file header from a buffer. */
57
- export async function parseRarHeader(data) {
58
- await ensureInit();
59
- return parse_rar_header(data);
60
- }
61
-
62
- /** Parse all RAR4 file headers from a buffer. */
63
- export async function parseRarHeaders(data) {
64
- await ensureInit();
65
- return parse_rar_headers(data);
66
- }
67
-
68
- /** Parse the first RAR5 file header from a buffer. */
69
- export async function parseRar5Header(data) {
70
- await ensureInit();
71
- return parse_rar5_header(data);
72
- }
73
-
74
- /** Parse all RAR5 file headers from a buffer. */
75
- export async function parseRar5Headers(data) {
76
- await ensureInit();
77
- return parse_rar5_headers(data);
78
- }
79
-
80
- // Re-export classes with aligned names (require manual init() before use)
81
- export {
82
- WasmRarArchive as RarFilesPackage,
83
- WasmRarDecoder as RarDecoder,
84
- WasmRar5Decoder as Rar5Decoder,
85
- WasmRar5Crypto as Rar5Crypto,
86
- };
87
-
88
- /**
89
- * Extract the first file from a RAR archive buffer.
90
- * Auto-detects RAR4/RAR5, parses headers, and decompresses in one call.
91
- * @param {Uint8Array} data - The entire RAR archive
92
- * @returns {Promise<{name: string, data: Uint8Array, size: number}>}
93
- */
94
- export async function extractFile(data) {
95
- await ensureInit();
96
- return extract_file(data);
97
- }
98
-
99
- // Re-export snake_case direct access (require manual init() before use)
100
- export {
101
- is_rar_archive,
102
- get_rar_version,
103
- parse_rar_header,
104
- parse_rar_headers,
105
- parse_rar5_header,
106
- parse_rar5_headers,
107
- extract_file,
108
- };
109
-
110
- /**
111
- * Create a Web ReadableStream from an async data source.
112
- *
113
- * This utility helps create streaming responses for browsers,
114
- * useful for Service Workers and fetch handlers.
115
- *
116
- * @param {Object} options
117
- * @param {number} options.totalSize - Total size of the data
118
- * @param {number} [options.start=0] - Start offset
119
- * @param {number} [options.end] - End offset (inclusive), defaults to totalSize-1
120
- * @param {number} [options.chunkSize=65536] - Size of each chunk to read
121
- * @param {function(start: number, end: number): Promise<Uint8Array>} options.readChunk - Function to read a chunk
122
- * @returns {ReadableStream<Uint8Array>}
123
- *
124
- * @example
125
- * // In a Service Worker fetch handler
126
- * const stream = createReadableStream({
127
- * totalSize: file.length,
128
- * start: rangeStart,
129
- * end: rangeEnd,
130
- * readChunk: async (start, end) => {
131
- * const decoder = new WasmRarDecoder(file.unpackedSize);
132
- * // ... fetch and decompress data
133
- * return decompressedData.slice(start, end + 1);
134
- * }
135
- * });
136
- * return new Response(stream, { headers: { 'Content-Type': 'video/mp4' } });
137
- */
138
- export function createReadableStream(options) {
139
- const { totalSize, start = 0, end = totalSize - 1, chunkSize = 64 * 1024, readChunk } = options;
140
- let offset = start;
141
-
142
- return new ReadableStream({
143
- async pull(controller) {
144
- if (offset > end) {
145
- controller.close();
146
- return;
147
- }
148
-
149
- const chunkEnd = Math.min(offset + chunkSize - 1, end);
150
- try {
151
- const chunk = await readChunk(offset, chunkEnd);
152
- controller.enqueue(chunk);
153
- offset = chunkEnd + 1;
154
- } catch (err) {
155
- controller.error(err);
156
- }
157
- },
158
- });
159
- }
160
-
161
- /**
162
- * Helper to create a streaming response for range requests.
163
- *
164
- * @param {Object} options
165
- * @param {number} options.totalSize - Total file size
166
- * @param {string} [options.rangeHeader] - HTTP Range header value
167
- * @param {string} [options.contentType='application/octet-stream'] - MIME type
168
- * @param {function(start: number, end: number): Promise<Uint8Array>} options.readChunk
169
- * @returns {{ stream: ReadableStream, headers: Headers, status: number }}
170
- *
171
- * @example
172
- * // In a Service Worker
173
- * const { stream, headers, status } = createRangeResponse({
174
- * totalSize: innerFile.length,
175
- * rangeHeader: request.headers.get('Range'),
176
- * contentType: 'video/mp4',
177
- * readChunk: async (start, end) => decompress(start, end),
178
- * });
179
- * return new Response(stream, { status, headers });
180
- */
181
- export function createRangeResponse(options) {
182
- const { totalSize, rangeHeader, contentType = 'application/octet-stream', readChunk } = options;
183
-
184
- let start = 0;
185
- let end = totalSize - 1;
186
- let status = 200;
187
-
188
- const headers = new Headers({
189
- 'Content-Type': contentType,
190
- 'Accept-Ranges': 'bytes',
191
- });
192
-
193
- // Parse Range header if present
194
- if (rangeHeader) {
195
- const match = rangeHeader.match(/bytes=(\d*)-(\d*)/);
196
- if (match) {
197
- start = match[1] ? parseInt(match[1], 10) : 0;
198
- end = match[2] ? parseInt(match[2], 10) : totalSize - 1;
199
- status = 206;
200
- headers.set('Content-Range', `bytes ${start}-${end}/${totalSize}`);
201
- }
202
- }
203
-
204
- headers.set('Content-Length', String(end - start + 1));
205
-
206
- const stream = createReadableStream({
207
- totalSize,
208
- start,
209
- end,
210
- readChunk,
211
- });
212
-
213
- return { stream, headers, status };
214
- }
package/lib/index.d.ts DELETED
@@ -1,119 +0,0 @@
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 DELETED
@@ -1,376 +0,0 @@
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
- // Streaming decompression not supported for custom FileMedia
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
- }