@match-kit/scoring
v0.1.0
Published
Score a compiled candidate against a target object with the objdiff engine: one scorer for every matching-decompilation tool
Readme
@match-kit/scoring
Score a compiled candidate against a target object, one symbol at a time, with the
objdiff engine (objdiff-wasm, pinned to an exact version).
Runs on Node ≥ 24, Bun and browsers with DisposableStack (Chrome ≥ 134, Firefox ≥ 141).
npm install @match-kit/scoringScore two objects
import { createScorer } from '@match-kit/scoring';
using scorer = await createScorer(); // loads the engine once per process or worker
using target = scorer.parseTarget(targetBytes); // parse once, score many candidates against it
const score = scorer.score(target, candidateBytes, 'MyFunction');
// { symbol, score: 3, match: false, rows: 14, matching: 11,
// breakdown: { insert: 1, delete: 0, replace: 0, opMismatch: 0, argMismatch: 2 } }scoreis the number of differing instruction rows;0is a byte-exact match.rowsis the scale the score is measured on. It belongs to the candidate's alignment, so two candidates of one function can have differentrows.- The breakdown sums to
score.
Score files on Node
import { releaseTarget, scoreFiles } from '@match-kit/scoring/files';
scoreFiles('build/target.o', 'tmp/candidate.o', 'MyFunction');scoreFiles is synchronous and keeps two memos, both keyed on file content:
- the parsed target, compared byte for byte, so a target rewritten in place is parsed again;
- each candidate's score, keyed by the candidate's SHA-256 and the symbol.
releaseTarget() drops both.
Show the rows
import { assembly, differences, sideBySide } from '@match-kit/scoring/display';
const inspection = scorer.inspect(target, candidateBytes, 'MyFunction');
console.log(sideBySide(inspection));
differences(inspection); // [{ row, kind: 'argMismatch', target: 'add r0, #0x1', candidate: 'add r0, #0x2' }, …]How a score is counted
- Sides. The target is objdiff's left side and the candidate its right, as in objdiff's own UI.
- Row kind. A row's kind is the target side's kind, else the candidate side's. A row counts as a difference when either side differs.
- Fails closed. Every failure throws:
SymbolNotFoundError(withside) when the symbol is missing from either object.UndiffableErrorwhen this pair cannot be diffed: an object that cannot be parsed, a row that cannot be displayed, a symbol with zero rows, or a row that does not decode as an instruction and would count as matching (objdiff diffs any two undecoded rows as matching; a wrongarm.archVersionmakes every row one). An undecoded row that differs anyway is counted. The next pair may still score.EngineFailedErrorwhen the engine itself has failed. Each panic leaks engine memory, and after a few thousand the engine fails every call. From then on every call in the process throws this: restart the process.
- Config. The
DiffConfigis objdiff's default unlesscreateScorer({ diffSettings })sets properties, andscorer.configKeyspells them. PutconfigKeyinto any cache key next toOBJDIFF_VERSION: both change what a score means. An invalid setting rejectscreateScorer. - Engine handles. Every handle a call creates is released before it returns. The parsed target
and the scorer's
DiffConfiglive until theirusingblock ends, or untildispose().
Bundlers
objdiff-wasm initializes with a top-level await and fetches its .wasm next to its own module.
With Vite, exclude it from dependency pre-bundling, or the dev server serves the wasm with the wrong
MIME type:
// vite.config.js
export default { optimizeDeps: { exclude: ['objdiff-wasm'] }, build: { target: 'es2022' } };