Matthew Jackson 4fcd28b487 Parse MKV lacing, route by real TrackNumber, honour NAL length size and edit lists
Four conformance defects in the read paths, two of them silent corruption.

**Lacing was ignored entirely.** RFC 9559 §10.2 defines Xiph, EBML and
fixed-size lacing, where one Block carries several frames; the reader took the
Block payload verbatim, so a laced Block became a single "frame" consisting of a
lacing header followed by concatenated frames — garbage to the codec parser, no
error. Audio tracks from other muxers commonly use lacing, so an ordinary
foreign MKV was silently mangled.

All three modes are now parsed: Xiph 255-run sizes including the trailing-zero
rule for exact multiples of 255, EBML unsigned first size plus SIGNED VINT deltas
with the 2^((7*n)-1)-1 bias of §10.3.3, and fixed-size even division, with the
last frame's size deduced from the remainder. Laced timestamps follow §10.3.5:
the first frame takes the Block timestamp and the rest are spaced by the track's
DefaultDuration, else BlockDuration/count, else shared with a warn.

Parsing was chosen over refusing because refusal would leave freemkv unable to
remux common foreign audio at all, and each mode is about fifteen lines.

A malformed lacing header now raises a NEW code, E_MKV_LACING_INVALID = 9052,
deliberately NOT MkvInvalid — because is_skippable_title_stub classifies
MkvInvalid as a skippable nav stub, so reusing it would have recreated the exact
conflation that is still open as a separate finding. A test asserts the new code
is not skippable.

**TrackNumber was assumed to be 1..N in TrackEntry order.** RFC 9559 §5.1.4.1.1
only requires it to be non-zero and unique, so sparse or unordered numbers are
legal. Block routing and codec_private both computed track + 1. A real
TrackNumber map is now built, recorded only for TrackEntries that yield a stream
so dropped track types no longer shift the mapping.

Verified red here independently, and the failure mode is worse than mis-routing:
with track + 1 restored, a buttons track's payload was attributed to the AUDIO
stream — wrong payload into the wrong codec parser.

**The NAL length prefix was hardcoded to 4 bytes.** lengthSizeMinusOne lives in
avcC byte 4 and hvcC byte 21 (ISO/IEC 14496-15 §5.3.3.1.2, §8.3.3.1.2) and was
never read, so a source declaring 1- or 2-byte prefixes had its raw prefixed
bytes emitted verbatim with no start codes. All four conversion sites now derive
the width from the track's own configuration record.

**Edit lists were ignored.** No edts/elst was parsed, so the presentation
timeline an edit list defines (ISO/IEC 14496-12 §8.6.5/§8.6.6) was dropped —
which is how encoder delay is normally expressed. Leading empty edits and the
first media edit's media_time are now applied to both dts and pts, with the movie
vs media timescale distinction respected. A list needing more than a constant
shift applies the leading edit and warns rather than presenting the result as
faithful.

17 tests. I reproduced the lacing mutant independently: returning the body whole
kills five of them, including the exact-payload and malformed-header cases.

Still open and deliberately untouched: the MkvInvalid / is_skippable_title_stub
conflation across ~20 reader raise sites. It is a cross-cutting error.rs change
and E_MKV_LACING_INVALID is the template for it.
2026-07-29 21:53:19 -07:00
2026-04-16 17:51:45 +00:00

License: MIT

libfreemkv

Rust library for 4K UHD / Blu-ray / DVD optical drives. Drive access, disc scanning, stream labels, AACS decryption, CSS decryption, KEYDB updates, and content reading in one crate. Drive-level unlocking is handled internally; consumers work with disc access and decryption only.

DVDs (CSS) decrypt out of the box. Blu-ray and UHD (AACS) require a keydb.cfg (default ~/.config/freemkv/keydb.cfg) supplying disc-specific volume unique keys; no AACS key material is compiled in.

12+ MB/s sustained read speeds on BD. Drive prep (init()) handles unlocking internally via the freemkv-unlock crate — clients never see it; when no drive unlock applies, the library rips via the host-certificate AACS handshake.

Multi-lingual by design — the library outputs structured data and numeric error codes, never English text. Build any UI or localization on top.

Source & API · Technical Docs

Part of the freemkv project.

Install

Consumed by git tag (not published to crates.io):

[dependencies]
libfreemkv = { git = "https://github.com/freemkv/libfreemkv", tag = "vX.Y.Z" }

Quick Start

use libfreemkv::{Drive, Disc, ScanOptions};
use std::path::Path;

// Open drive — identified via INQUIRY
let mut drive = Drive::open(Path::new("/dev/sg4"))?;
drive.wait_ready()?;              // wait for disc
drive.init()?;                     // unlock + prep (handled internally)
drive.probe_disc()?;               // probe disc surface for optimal speeds

// Scan disc — UDF, playlists, streams, AACS (all automatic)
let disc = Disc::scan(&mut drive, &ScanOptions::default())?;

for title in &disc.titles {
    println!("{}{} streams", title.duration_display(), title.streams.len());
}

// Stream pipeline — read PES frames from any source, write to any output
let opts = libfreemkv::InputOptions::default();
let mut input = libfreemkv::input("iso://Disc.iso", &opts)?;
let title = input.info().clone();
let mut output = libfreemkv::output("mkv://Movie.mkv", &title)?;
while let Ok(Some(frame)) = input.read() {
    output.write(&frame)?;
}
output.finish()?;

Multi-pass recovery rip

Recovery moved OUT of this crate in 1.6.0. The sweep/patch strategy, the ddrescue mapfile, damage classification and the multipass loop now live in the freemkv-engine crate as freemkv_engine::recovery::{copy, sweep, patch}.

libfreemkv keeps the layers underneath: the raw single-shot read (Drive::read) and the SCSI-fact translation (SenseFamily) that the engine's strategy is built on. The dependency runs engine → libfreemkv, so this crate cannot call into it; front-ends get recovery from the engine directly. See docs/rip-recovery.md for what stayed here.

What It Does

  • Drive access — open, identify, internal unlock + prep, speed control, eject
  • 12+ MB/s reads — auto-detects kernel transfer limits, sustained full speed
  • Disc scanning — UDF 2.50 filesystem, MPLS playlists, CLPI clip info
  • Stream labels — 5 BD-J format parsers (Paramount, Criterion, Pixelogic, CTRM, Deluxe)
  • AACS decryption — transparent key resolution and content decrypt (1.0 + 2.0 bus decryption)
  • KEYDB updates — download, verify, save from any HTTP URL (zero deps, raw TCP)
  • Content reading — adaptive batch reads with automatic decryption
  • Stream I/O — unified stream pipeline for reading and writing any format

Streams

Stream Input Output Transport
DiscStream Yes -- Optical drive via SCSI
IsoStream Yes -- Blu-ray ISO image file (read via stream pipeline; written by freemkv_engine::recovery)
MkvStream Yes Yes Matroska container
M2tsStream Yes Yes BD transport stream with FMKV metadata header
NetworkStream Yes (listen) Yes (connect) TCP with FMKV metadata header
StdioStream Yes (stdin) Yes (stdout) Raw byte pipe
NullStream -- Yes Discard sink (byte counter for benchmarks)

Streams implement a single unified pes::Stream trait (re-exported as PesStream) exposing read() and write() on one type. input() / output() resolve URL strings to PES stream instances. All URLs use the scheme://path format — bare paths are rejected.

Keys

DVDs (CSS) decrypt out of the box, with no external key file needed.

Blu-rays and UHD (AACS) require a keydb.cfg at ~/.config/freemkv/keydb.cfg (or passed via ScanOptions). No AACS key material is compiled into the binary.

Architecture

Drive                  — open, identify, init, single-shot read
  ├── ScsiTransport    — SG_IO (Linux), IOKit (macOS), SPTI (Windows)
  └── unlock_bridge    — private seam to the freemkv-unlock crate
                         (firmware / AACS cert / CSS bus-auth unlockers)

Disc                   — scan titles, streams, AACS/CSS state
  ├── UDF reader       — Blu-ray UDF 2.50 with metadata partitions
  ├── MPLS parser      — playlists → titles + clips + streams
  ├── CLPI parser      — clip info → EP map → sector extents
  ├── IFO parser       — DVD title sets, PGC chains, cell addresses
  ├── Labels           — 5 BD-J format parsers (detect + parse)
  ├── AACS             — key resolution + content decryption
  ├── CSS              — DVD CSS (bus auth → player-key disc crack → known-plaintext title-key attack)
  └── KEYDB            — download + verify + save

Streams                — unified PES pipeline
  ├── PesStream        — pes::Stream: one trait, read()/write() PES frames
  ├── DiscStream       — sectors → decrypt → TS demux → PES
  ├── IsoStream        — ISO file → decrypt → TS demux → PES
  ├── MkvStream        — MKV mux/demux
  ├── M2tsStream       — BD transport stream
  ├── NetworkStream    — TCP with FMKV metadata header
  ├── StdioStream      — stdin/stdout pipe
  └── NullStream       — discard sink

See docs/ for detailed technical documentation on each module.

Error Codes

All errors are structured with numeric codes. No user-facing English text — applications format their own messages.

Range Category
E1xxx Device errors (not found, permission)
E2xxx Profile errors (unsupported drive)
E3xxx Unlock errors (failed, signature)
E4xxx SCSI errors (command failed, timeout)
E5xxx I/O errors
E6xxx Disc format errors
E7xxx AACS errors
E8xxx KEYDB update errors
E9xxx Stream / mux errors (URL, PES, ISO, pipeline, demux)

Platform Support

Platform Status Backend
Linux Supported SG_IO ioctl
macOS Supported IOKit SCSITask
Windows Supported SPTI

Contributing

Run freemkv info disc:// --share with the freemkv CLI to capture your drive's identity for contribution. Drive-unlock profiles are maintained in the freemkv-unlock repository.

License

MIT

S
Description
Internal mirror of libfreemkv 1.1-beta HEAD for private kdb builds (tag drops flate2; kdb needs HEAD symbols)
Readme MIT
13 MiB
Languages
Rust 99.6%
C 0.3%
Shell 0.1%