@voxary/osr-parse
v0.1.0-alpha.3
Published
Native N-API parser for osu! .osr replay files
Readme
osr-parse
Native N-API parser for osu! .osr replay files. Ships prebuilt
binaries for Windows/macOS/Linux (x64 + arm64), falls back to compiling
from source otherwise. Header parsing, LZMA-alone decompression, and
frame parsing all happen off the main thread; the result comes back as
typed arrays (SoA), not a JS object per frame.
Safety is the actual point of this package: every read is bounds-checked
against the input buffer before it happens, size fields are validated
against hard caps before anything is allocated, and no C++ exception can
escape across the N-API boundary — a malformed or hostile .osr file
produces a rejected Promise with a typed error code, never a crash.
Install
npm install @voxary/osr-parseIf no prebuild matches your platform/arch, npm install compiles from
source automatically (needs a C++17 compiler + CMake — see
BUILDING.md).
Usage
const { parse } = require('@voxary/osr-parse');
const fs = require('node:fs');
const buffer = fs.readFileSync('replay.osr');
const replay = await parse(buffer);
console.log(replay.header.playerName, replay.header.modsList); // ['HD', 'DT']
console.log(replay.frames.times.length, 'frames');
// SoA typed arrays, not one object per frame:
const { times, x, y, keys } = replay.frames;
for (let i = 0; i < times.length; i++) {
// times[i]: ms since previous frame
// x[i], y[i]: cursor position
// keys[i]: pressed-key bitmask
}
if (replay.rngSeed !== null) {
console.log('RNG seed:', replay.rngSeed);
}Errors are typed and never thrown as a bare string:
try {
await parse(corruptBuffer);
} catch (err) {
err.code; // e.g. 'ERR_OSR_HEADER_TRUNCATED', 'ERR_OSR_LZMA_DECODE_FAILED', ...
err.message;
}Full type definitions, including every OsrErrorCode, are in
index.d.ts.
Mods
header.mods is the raw bitmask; header.modsList is it decoded into
acronyms (['HD', 'DT']). The bit→acronym table is also exported
directly if you need to decode a bitmask yourself:
const { MOD_BITS, decodeMods } = require('@voxary/osr-parse');
decodeMods(8 | 64); // ['HD', 'DT']Safety guarantees
- Every binary read goes through a bounds-checked accessor
(
BufferReader); a truncated file raises a typed error, never reads past the buffer. - Size fields (string length, compressed block length, LZMA dictionary size, decompressed size, frame count) are validated against fixed caps before anything is allocated — a file that lies about its own size can't trigger an out-of-memory DoS.
- No raw
new/mallocwithout matching RAII; the LZMA decoder state is a scoped C++ object even though the underlying SDK is C. - Every C++ exception is caught before it can cross into JS
(
Napi::AsyncWorker's own exception guard, reinforced by explicittry/catcharound the parse call — seesrc/parse_worker.cc). - Continuously fuzzed: libFuzzer harnesses on the header and frame
parsers run on every PR (short) and nightly (long), seeded with real
and deliberately-corrupted
.osrfiles — see fuzz/. - ASan + UBSan run against the full test suite in CI on every change — see .github/workflows/sanitizers.yml.
A bad file is always a clean rejected Promise with an ERR_OSR_* code —
never a segfault, never memory corruption, never a crashed process.
Benchmark
osr-parse vs. osu-parsers
(a pure-JS parser), both parsing the same 50,000-frame .osr fixture
via their public async buffer-decoding API. Methodology: 5 warmup
iterations, then 50 timed iterations of a full parse from a fresh
buffer each time; see bench/bench.js for the exact
code. Reproduce with npm run bench.
node v26.7.0, warmup=5 iterations=50
osr-parse : min=6.788ms median=7.878ms mean=7.829ms max=8.847ms
osu-parsers: min=1239.708ms median=1278.153ms mean=1273.611ms max=1323.596ms
osr-parse is 162.2x faster (median)(Measured on the machine this package was developed on; run
npm run bench yourself for numbers on your hardware — that's the
point of it being reproducible rather than a fixed claim.)
Supported platforms
Prebuilds are published for:
| OS | x64 | arm64 | |---------|:---:|:-----:| | Linux | ✅ | ✅ | | macOS | source build¹ | ✅ | | Windows | ✅ | ✅ |
¹ macOS x64 (Intel) has no free GitHub-hosted CI runner as of this
writing, so no bundled prebuild ships for it; npm install compiles
from source automatically (needs Xcode Command Line Tools).
Any other platform/arch falls back to the same automatic source build.
Node.js >= 22 (Active LTS and newer) is required; the native addon itself is Node-API/ABI-stable so a single prebuild per platform/arch covers every supported Node version.
Building from source / contributing
See BUILDING.md for the full local dev setup (including why the LZMA SDK is vendored as a git submodule instead of a vcpkg port), fuzz/README.md for fuzzing, and CHANGELOG.md for release history.
License
MIT, see LICENSE. Third-party licenses (vendored LZMA SDK, etc.) are in THIRD_PARTY_NOTICES.md.
