@bitplane/rars
Read, write and repair RAR archives in browsers and Node.
npm install @bitplane/rars
Read
import { RarArchive } from "@bitplane/rars";
const archive = await RarArchive.open(file, { password: "optional" });
for (const entry of archive.entries) {
console.log(entry.name, entry.size, entry.isEncrypted);
}
const bytes = await archive.get("docs/readme.txt").bytes();
await archive.test();
archive.close(); // optional; releases retained input eagerly
open() accepts a Blob, ArrayBuffer or typed array. Pass an ordered array
to open a volume set. Entry objects use archive-order indices internally, so
duplicate names and non-UTF-8 names remain unambiguous; nameBytes contains
the exact header bytes.
get(name) selects the last matching entry; getAll(name) returns every match
in archive order. Each returned entry reads its own payload by index.
Node also accepts paths and file URLs. Passing the first path of a conventional volume set discovers its siblings automatically.
For legacy byte names, pass legacyNameEncoding: "cp850" to open().
Supported encodings are cp437, cp850, cp852, cp866, windows-1251,
windows-1252 and utf-8 (cp1251, cp1252 and utf8 are aliases).
Names are case insensitive. Unicode names take precedence; unspecified legacy
encodings are never guessed. Invalid or undefined input fails conversion.
entry.name is the selected view and entry.nameBytes retains its original
identity. Reopen to select another encoding. get() refuses ambiguous decoded
names with AMBIGUOUS_ENTRY; use getAll() or archive-order entry indices.
This setting does not affect comments, passwords, payloads or writer names.
open(), entry.bytes(), readMany(), test() and readComment() accept ReadOptions:
const limits = { maxHeaderCount: 10000, maxMemberOutputBytes: 256 * 1024 * 1024 };
const archive = await RarArchive.open(file, limits);
const bytes = await archive.entries[0].bytes(limits);
const selected = await archive.readMany([archive.entries[1], archive.entries[0]], limits);
const comment = await archive.readComment(limits); // undefined if absent
The optional fields are maxHeaderCount, maxHeaderBytes,
maxMemberOutputBytes, maxTotalOutputBytes, maxReaderWorkspaceBytes, rar50DictionarySizeLimit and
rar50BufferedDecodeLimit. Values must be nonnegative safe integer numbers or
bigint values through 2n ** 64n - 1n. Zero is a real limit. Omission retains
the library default. Resource options apply to one call and are not retained by open().
Each operation reparses its input; header limits apply per archive/volume.
readMany() accepts an iterable of entries or names and performs one parse and
decoder traversal for the batch. Results retain request order and duplicates;
prefer entry objects for duplicate identity and direct index selection. Entries
from another archive are rejected. Solid predecessors are decoded once, and
output limits apply to the whole batch. Repeated results have independent byte
buffers but do not consume logical decode quota again. Failures return no partial
list. Directories are rejected; RAR5 redirections have no readable file payload.
The cached archive.comment is decoded under the options passed to open();
readComment() decodes it again with fresh options and an optional password.
Output limits count logical decoded bytes, including discarded solid-prefix output. Reading a volume entry currently decodes the whole volume set, so its total-output limit includes other members. Limits are not an aggregate RAM ceiling: inputs and returned outputs are materialised, and decoder workspace has separate costs. RAR5/7 dictionary and buffered-decoding limits affect those formats only. Filesystem scratch decoding is unavailable in the WASM backend. Unknown-size RAR5 members are refused under a member-output limit; decode-to-end support remains separate work.
maxReaderWorkspaceBytes limits aggregate reader allocation capacity per call,
including decoder state and queued parallel payloads. Source buffers, JavaScript
objects and returned bytes are outside the quota. Capacity replacement overlap
counts; this is not a process RAM limit. Refusals retain the resource error code
and byte details.
Write
import { RarWriter } from "@bitplane/rars";
const writer = new RarWriter({ format: "rar50", level: 5 });
writer
.add("hello.txt", "hello")
.add("data.bin", payload, { mode: 0o644, modifiedAt: new Date() });
const bytes = await writer.bytes();
const parts = await writer.volumes(5 * 1024 * 1024);
level runs from 0 (stored) to 5. Other options are solid, password,
encryptHeaders, comment and recoveryPercent. Node additionally provides
addFile(), writeTo() and writeVolumesTo().
For legacy formats, writeVolumesTo() keeps the first path and names later
volumes .r00 through .r99 in the same directory. Sets exceeding 101 volumes
or containing overlapping output paths are rejected before any file is written.
Long operations accept an AbortSignal and progress callback:
const bytes = await writer.bytes({
signal: controller.signal,
onProgress: ({ phase, completed, total }) => {
console.log(phase, completed, total);
},
});
Errors are RarError instances with a stable code; client AbortSignal
cancellation uses the standard AbortError. Core errors carry their code through
WASM and the worker without parsing message text. details.contexts retains
entry names as raw byte arrays, operations, archive offsets and volume numbers.
Resource-limit details use decimal strings for byte counts, preserving values
beyond JavaScript’s exact integer range. Node filesystem errors use IO with
the original system code, errno and syscall in details.
Aborting queued work rejects it immediately without interrupting the active operation. Aborting active work terminates its worker; later operations start a fresh worker. This is worker termination, so it does not run cooperative Rust cleanup or undo completed filesystem writes. Interrupted Node writes may leave staged temporary files. No separate cancellation-token object is needed in JS.
RarWriter accepts maxMemoryBytes as a nonnegative safe integer or a bigint
through 2n ** 64n - 1n. It caps managed RAR5/7 execution allocation capacity,
including retained WASM spools, archive/volume output and the final JavaScript
payload copy. The default is unlimited; zero admits no managed allocation.
A refusal has code RESOURCE_LIMIT; legacy formats reject the requested policy
with UNSUPPORTED_FEATURE. Caller inputs, returned caller-owned bytes, JavaScript
runtime objects, allocator overhead, stacks and OS memory are excluded. This is
not a process-memory ceiling and does not bound reading or rewrite staging.
Loading
ES modules, Node require, Vite, webpack, Rollup and direct browser imports use
the same API and require no explicit Wasm initialisation:
<script type="module">
import { RarArchive } from "https://esm.sh/@bitplane/rars";
</script>
Direct cross-origin imports use a small blob worker bootstrap. A restrictive
Content Security Policy must therefore allow blob: in worker-src, or the
package assets should be bundled or served from the page's origin.
The current API materialises input and output bytes. True backpressured stream input/output will be added separately rather than presenting a stream that secretly buffers an entire archive.
The same library is available as a Rust crate, Python package and command-line tool.
Development validation
Run CARGO_BUILD_JOBS=1 scripts/build-npm.sh --debug and then
node scripts/test-npm-package.js for focused engine and worker checks.
The debug build skips LTO and wasm-opt. Build release artifacts with
scripts/build-npm.sh without --debug. Both modes require a wasm-bindgen
CLI matching Cargo.lock.
Development coverage
From the repository root, run python3 scripts/coverage-wasm.py to measure
Rust engine coverage under Node. It requires Node, wasm-bindgen matching
Cargo.lock, and a Rust nightly using LLVM 23 with wasm32-unknown-unknown
and llvm-tools-preview installed. The command uses one build job and at most
two available CPUs, and saves reports under the ignored
target/coverage-wasm-focused directory.
An independent two-branch probe checks the measurement before testing the
engine. Private error conversion tests are compiled with cfg(test);
their exports and the profiling runtime stub are absent from release builds.
This measures Rust WASM source, separately from JavaScript facade coverage.