Files
libfreemkv/CHANGELOG.md
T
Matthew Jackson b68765fe84 Stop naming specific commercial discs in comments and tests
Fifteen references across six files named the discs a defect was first
seen on. The parser leak found earlier was not an isolated slip — the
same habit runs through the mux comments, the changelog and the AACS
content verdict, where a title name was standing in for the shape of
the problem.

Every one is replaced with the property that actually mattered: a
multi-clip title, a UHD Dolby Vision profile 7 dual-layer stream, a
disc carrying an authored-bad TS packet. The comments are more useful
for it — the reader needs to recognise the shape on a disc they have,
not the one we happened to have.

`SEG_MainFeature` stays: the parser matches on that literal, so it is
a format token rather than a title.
2026-08-02 17:00:10 -07:00

1394 lines
82 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
## [1.6.0] — UNRELEASED
### Fixed
- **An `.fvi` index named itself as its own source.** The `fvi://` sink was
handed the DESTINATION path as its `source_path`, so every index reported
`source.path` as the file it was writing. `source.medium` was always `file`
whatever the real source, and `source.title` always `0` whatever title was
muxed — `docs/FVI_FORMAT.md` §6 defines all three as describing the input.
Beyond the wrong data, it made the format non-reproducible: two machines
indexing identical bytes produced different files purely because they wrote
to different paths, and a local filesystem path leaked into a shareable file.
`output()` now takes the source provenance explicitly. One of the crate's own
tests had been asserting the wrong value, which is why the suite never caught
it.
- **Vendor stream labels were numbered by parsed entry, not by stream slot.**
Label blobs contain entries the parser deliberately does not interpret, but
those entries still occupy a stream-number slot. Counting only the parsed
ones shifted every later label up by the number skipped, so on discs with an
uninterpreted entry early in the list the language, forced, SDH and
commentary flags were attached to the wrong tracks — a subtitle track could
present as both a plain and a forced variant of the same language, with the
forced flag landing on neither or both. Four parsers shared the defect
(`pixelogic`, `paramount`, `mpls_universal`, `deluxe`); the rest are now
pinned by tests proving they are immune. Three of the crate's own tests had
been asserting the shifted numbering.
- **A feature's stream list ran on past its end and picked up menu clips as
streams.** The parser for one vendor's label blob finds the feature
playlist's section by name and ends it at the next named section — but on
most discs of that authoring style the feature playlist IS the last named
section, and the trailing per-language notice, disclaimer and dub-credit
cards carry no name marker at all. The walk therefore swallowed the whole
tail of the blob as more of the feature's own stream list. Those cards are
named per language, in the same shape as a stream token, so each one silently
advanced a stream-number counter, and the ones whose name collided with a
catalogued component were labelled as streams outright — on one disc, five
audio labels for slots 10 to 14 of a playlist that has nine. The same
collisions were being reported as vocabulary gaps and cost that disc's parse
its high-confidence rating. A section now also ends where the next one's
stream list begins, which is at a video slot the section has already listed.
Eight of eleven affected-format discs in the test corpus have no terminating
marker; two of them were producing labels for streams their feature playlist
does not contain. No other label parser walks a flat entry sequence this way
— the rest scope each stream to a structural range or read its number off the
entry itself, and two more now carry tests pinning that.
- **A forced-narrative subtitle marker went uncatalogued.** The token marking
the signs-and-on-screen-text pass that accompanies a dubbed presentation was
not in the vocabulary, so that track lost its forced flag while every other
language in the same run kept theirs. Vocabulary gaps are also no longer
silent: an unrecognized component now produces one aggregated warning per
parse naming the distinct components, rather than a bool nobody could see.
- **A key service that was DOWN was reported as "this disc has no key".** A
failed lookup and a successful lookup that found nothing shared a match arm,
so an unreachable service, a rejected token and a rate limit all arrived as
"no entry". New codes distinguish them: `E7028` service unreachable or 5xx,
`E7029` token rejected, `E7030` rate limited. An operator hunting a missing
VUK during a transient outage was the actual, observed cost.
### Breaking
- **`Resolution::pixels()` now returns `Option<(u32, u32)>`, not a bare tuple.**
The previous sentinel for "unresolved" was `(0, 0)` — a pair every caller
could mistake for a usable value, and one did (see `mp4` under Fixed). Every
caller now has to choose what an unresolved resolution means for it: the
Matroska and metadata sinks take `.unwrap_or((0, 0))` with the reason stated
at each site, the VobSub writer degrades to a palette-only `.idx`, and the
MP4 sink refuses the track.
- **`DiscSession::into_drive()` returns `Result<Drive, Error>`, not a bare
`Drive`.** The empty-slot state it used to panic on is reachable through
ordinary public use (`stage_drive_as_reader` moves the drive out; calling
`into_drive()` twice moves it out again), so the panic was not guarding
caller error — it was guarding a legitimate second call.
- **`DiscSession::drive()` / `drive_mut()` removed.** Dead public API with
zero callers anywhere in the toolchain, and panicking accessors are not
worth preserving the shape of.
- **`clpi`: the unused EP-map → sector-extent path deleted** (`get_extents`,
`resolved_ep_map`, `full_pts`, `full_spn`, `parse_cpi`, `EpCoarse`,
`EpFine`, and the `ep_coarse`/`ep_fine`/`version` fields on `ClipInfo`).
Crate-internal (`pub(crate)`), so not source-breaking for an external
consumer, but listed here because it removes surface: nothing in the
toolchain called it, it carried a truncation bug (fixed in a prior commit,
then removed with the code it fixed), and `ClipInfo` keeps only
`source_packet_count` and `streams`.
- New error codes: **E9055** (`Mp4UnknownResolution`), **E9056**
(`SyncTimeout`), **E9057** (`SyncWorkerLost`). Front-ends rendering error
strings need entries for all three.
### Added
- **High-level orchestration API — a single mux driver and a disc session.**
`mux_stream` drives the whole read → decrypt → demux → write pipeline for any
source (`MuxInput::Url` / `Iso` / `Session` / `Live`), so consumers stop
hand-rolling the frame pump. `DiscSession` hoists drive open + SCSI bring-up +
scan + key resolution behind one type; `scan_iso` does the same for a
file-backed ISO; `resolve_keys` / `resolve_keys_for` resolve base AACS keys.
These let the CLI and autorip shrink to thin front-ends (and back the new
`freemkv-engine` crate).
- **Per-title stream selection (`StreamSelection`).** A pure primitive that
prunes a `DiscTitle`'s audio/subtitle streams to a chosen set of PIDs (video
is always kept) before the mux builds its demux state — so track headers,
`codec_privates`, and frame routing all follow the pruned list, with no
demux-internal filter. Carried on `MuxOptions.selection` /
`InputOptions.selection` (both default to keep-everything, a no-op).
Languages are the caller's concern; the library speaks PIDs.
- `DiscTitle::audio_streams()` / `subtitle_streams()` / `video_streams()`
typed iterators over each stream class.
- `MuxOptions` gains a per-call write-pipeline `send_deadline` and derives
`Default`.
### Changed
- **The recovery strategy moved to the new `freemkv-engine` crate.** Sweep,
patch, the retry-decision state machine, mapfile bookkeeping, and damage
classification are freemkv's specific recovery *philosophy*, not disc-access
primitives — they now live in `freemkv-engine`, which composes libfreemkv's
public API. The library keeps the raw single-shot read, SCSI-sense-fact
translation (`SenseFamily`, now in `scsi`), decrypt, and the mux highway.
- Small deliberate `pub` promotions to support the engine as an external
consumer: `Disc::resolve_content_key_map` / `encrypted_content_ranges`,
`io::WritebackFile`, `drive::extract_scsi_context`, `disc::locate_ranges`.
- New typed error classifiers re-exported at the crate root — `is_halt`,
`is_skippable_title_stub`, `is_disc_level_no_key` — and a new error variant
`SelectionPidUnknown` (E6014).
### Fixed
- **An undecryptable CSS disc no longer exits successfully.**
`Error::CssKeyMissing` (E7023) carried two conditions needing opposite
responses: one title of a multi-VTS DVD failing its own re-crack — correctly
skippable, the rest of the disc still rips — and the *whole disc* failing its
crack (`Disc::css_error`), where every title fails identically. Both raised
E7023, which `is_skippable_title_stub` classifies, so an uncrackable disc
iterated all N titles logging "title skipped" and exited 0. The disc-wide gate
(`Disc::ensure_decryptable[_keys]`) now raises `Error::CssNoDiscKey`
(**E7027**) — the CSS analogue of `NoDiscKey` (E7022), classified by
`is_disc_level_no_key`, so a rip loop fails fast. The per-title raise keeps
E7023 and stays skippable.
- **A corrupt `mkv://` input is no longer reported as a title worth silently
skipping.** `Error::MkvInvalid` (E6008) carried two unrelated meanings: the
genuine "this title produced no muxable frames" stub — which
`is_skippable_title_stub` classifies as skippable, so an all-titles rip drops
the title and finishes the rest — and *every* malformed-input rejection in the
MKV read path. A truncated file, a bad VINT, a cluster timestamp past
`i64::MAX`, a BlockGroup child overrunning its group: all of them classified as
skippable, so a broken source was passed over by a run that then exited
successfully. The read path now raises `Error::MkvSourceInvalid`
(**E9053**) — the counterpart of `Mp4Invalid` (E9049) — and the writer's
unrepresentable-element-size guards raise `Error::MkvUnencodable` (**E9054**).
Neither is skippable. `E6008` now means only the no-muxable-frames stub (the
mux driver's headers-never-resolved gate and the MKV muxer's zero-frame
`finish()` guard). The `json://` sink's metadata-encoding guard, which also
raised `MkvInvalid`, now raises `NoMetadata` (E9008) like `mux::meta`'s.
Front-ends rendering error strings need entries for E9053 and E9054 (and for
E9051 / E9052, split off `E6008` earlier in this cycle for the same reason).
- **The FMTS (AACS 2.1) forensic key resolution now runs once per disc, not once
per title.** `Disc::resolve_content_key_map` resolves every title, and the FMTS
branch ran ahead of everything else — so each playlist re-walked the UDF
filesystem to re-read `/AACS/IndividualSegment.tbl`, re-ran the forensic anchor
probe and the per-index phase probe, and **re-asked the key service for the
disc's index-key set**. On a 60-playlist AACS 2.1 disc that was 60 identical
key-service round trips (a key-server storm) and tens of thousands of random
6144-byte reads for one disc-wide answer. The UDF walk is now memoised for the
whole disc and the index keys + phases per distinct extent list — the only
per-title input to the probes. A read-faulted phase probe is deliberately never
memoised, so one bad read is not spread across the remaining playlists. The
per-title UDF walk was paid on **every** disc, FMTS or not, so a plain BD sweep
loses ~59 full-stroke seeks too. The multi-CPS extent memo added in 1.6.0 is
also reachable on an FMTS disc for the first time.
- **Mux correctness pass** (the `v1.4.0..HEAD` 10-phase audit): DTS core-header
false-drops that dropped good DTS frames; the TrueHD channel-correction probe
now runs correctly on AACS discs (7.1/Atmos no longer understated as 5.1);
an FMTS phase-probe read fault is distinguished from a wrong key; multi-CPS
and orphan-clip keying; the AACS key map is now a *positive* map (a sector
with no key passes through rather than failing), with fail-loud on genuinely
unresolvable keys; a user Stop mid-read is reported as `completed = false`
(a stop is not a failure), not a spurious error.
- **`udf`: deleted files and directories are no longer read as if they still
existed.** File-characteristics bit 2 (Deleted, ECMA-167 4/14.4.4) was never
decoded, so a deleted File Identifier Descriptor's ICB was followed like any
other. A deleted FID is permitted to point at extent length zero — not at a
File Entry at all — so following it reads whatever descriptor happens to sit
at that LBA: a deleted *directory* landed on the File Set Descriptor and
failed enumeration of the **entire volume**; a deleted *file* read back as a
genuine zero-byte entry.
- **`udf`: the Metadata File is now located from the Metadata Partition Map,
not assumed to sit at block 0.** UDF 2.50 2.2.10 records a
partition-relative Uint32 at offset 40 of the map that is the only
authoritative answer to where the file lives; block 0 is merely where most
authoring tools happen to put it. A conformant volume that recorded it
elsewhere was rejected as not-a-UDF-filesystem, or — on a volume carrying a
decoy descriptor at block 0 — silently mounted the wrong filesystem and
reported success. Block 0 stays in the candidate chain, so a volume with no
map or a wrong one keeps mounting exactly as before; the map is trusted only
when its partition type identifier actually reads `*UDF Metadata Partition`
(a Virtual or Sparable map is also ECMA-167 Type 2 and records unrelated
fields at the same offset).
- **`udf`: a read fault while locating the Metadata File is now a read error,
not "not a UDF disc."** The candidate-loop fix above discarded the
distinction between "read fine and the bytes say no" (structural) and
"could not read" (transient) — a single marginal-sector fault fell through
to the block-0 fallback, mis-tagged, and came back as
`Error::UdfNotFilesystem`. `mux::resolve` **memoises that verdict for the
whole disc**, so one flaky read silently demoted every remaining title to
the base-Unit-Key-only path on an AACS 2.1 forensic disc. The same
read-fault-vs-negative-verdict fix is applied to the Volume Descriptor
Sequence fallback below.
- **`udf`: `file_start_lba` no longer returns an unrecorded extent's LBA.**
A prior change correctly started retaining ECMA-167 4/14.14.1.1 type-1
(allocated-but-not-recorded) extents rather than dropping them — dropping
one slides every later extent down by the hole's length — but
`file_start_lba` still took the *first* extent unconditionally, so it could
hand back a hole's LBA rather than where the file's data actually begins.
`ifo.rs` uses this value as the base for every VTS VOB extent
(`file_start_lba(IFO) + vtstt_vobs + cell.first_sector`), so a DVD whose IFO
opens with a type-1 descriptor read its **entire video title set from the
wrong sectors** — no error anywhere, the reads just landed on unrelated
data.
- **`udf`: the Volume Descriptor Sequence fallback now retries on outcome, not
on the anchor's declared shape.** The Main VDS was selected from the
anchor's declared extent whenever the extent's *shape* was usable (length,
location, no address wrap) and the customary fixed location was tried only
when the shape itself failed. Shape is a property of the field, not of what
is actually there: a stale anchor, a rewritten volume, or deliberate
corruption can pass every shape check and point at nothing, in which case
the sweep finds no Partition Descriptor and the volume is rejected — on
exactly the damaged-disc branch recovery exists for, with no recovery path.
Both locations are now candidates and the fallback fires on whether
following the anchor actually found anything.
- **`css`: a sector whose cached title key is proven stale and whose re-crack
fails is no longer descrambled with the stale key anyway.** That produced
garbage payload behind an intact clear header instead of a hard failure.
Raises `Error::DecryptFailed`, matching AACS's behaviour on the same
condition.
- **`decrypt`: an encrypted unit outside every key-map range no longer passes
through as ciphertext counted as good bytes.** `extract` could report a
scrambled file as complete with exit code 0.
- **`mux`: frames dropped by the resync gate now reach `errors()`.** The gate
zeroes its counter at each resync, so a gap that resolved was previously
invisible to the error count even though frames were genuinely lost.
- **`mux`: a discard by the 8 MiB access-unit backstop now marks the following
unit discontinuous.** Previously the resync gate never armed after corrupt
input recovered via the backstop, so the discontinuity that should have
triggered a resync went undetected.
- **`mp4`: a video track with no resolved resolution is now refused
(`E_MP4_UNKNOWN_RESOLUTION`, E9055) instead of written as a structurally
valid, unrenderable 0x0 track.** `Resolution::pixels()`'s old `(0, 0)`
sentinel for "unknown" was indistinguishable from a real answer, and MP4 has
no optional-element mechanism to omit the field the way Matroska does —
`tkhd` and `VisualSampleEntry` both make width/height mandatory. See
`pixels()` under Breaking.
- **`labels`: Deluxe master-enum selection no longer iterates a `HashMap`.**
Iteration order over a `HashMap` is unspecified, so the same disc could emit
different commentary/SDH labels on different runs. Selection is now
deterministic.
- **`io`: a cancelled rip during the bounded fsync is no longer reported as a
hard I/O failure.** The three bounded-fsync failure modes returned bare
`io::ErrorKind` values with no `E<code>` prefix, so `is_halt()` — which only
recognises that prefix — could not tell a user Stop from a wedged NFS mount
from a lost worker thread, and a genuine cancel surfaced as a hard failure
at the end of an otherwise-complete mux. They now carry distinct codes:
`Error::Halted`, `Error::SyncTimeout` (**E9056**), and
`Error::SyncWorkerLost` (**E9057**) — see New error codes under Breaking.
- **`session`: `into_drive()` is fallible; `drive()` / `drive_mut()` deleted.**
See Breaking.
- **`clpi`: the unused EP-map → sector-extent path deleted**, including a
truncation bug it carried (`out_time` past the last EP entry resolved short
of EOF). See Breaking.
### Tests
- The suite grew from ~2,570 to **2,994** tests over this cycle.
- A long-standing intermittent failure (roughly 1 run in 10 under the full
parallel suite) was diagnosed and removed. It came from asserting on a
`tracing` capture: the capturing subscriber is installed thread-local while
`tracing`'s callsite-interest cache is global, so the two could race
regardless of how carefully the capture was serialised. The predicate is
now a named function tested as a plain value, with no subscriber involved.
Measured clean afterward: 14 consecutive full-suite runs, 2,994 passed, 0
failed.
## [1.5.2] — 2026-07-22
### Fixed
- TrueHD 7.1/Atmos channel correction now works on AACS-encrypted (Blu-ray/UHD)
discs. The channel-correction probe was built without an AACS key map, so on
every AACS disc its first read failed and the correction was silently skipped —
a 7.1/Atmos TrueHD track was muxed with its MPLS-declared channel count (often
understated 5.1). The probe now resolves and installs the same key map the mux
read uses.
- AACS 2.1 (FMTS) discs: a non-forensic title (menu/extras playlist, or any clip
that carries no forensic segments) no longer hard-fails the rip. `resolve_fmts_key_map`
now filters segments to those addressable within the title and falls back to the
base Unit-Key map when none apply — previously the first non-forensic title
aborted the whole-disc decrypt and blocked muxing any non-main title. A
forensic phase probe whose sampled units are all source-zero padding (an
even/odd tie) no longer aborts the rip either.
- Multi-CPS AACS `dir://` extraction now decrypts each clip with its own CPS-unit
key instead of keying the whole disc with unit key 0 (which silently wrote
secondary-CPS files as garbage). A missing key fails loud at resolve. Single-CPS
extraction is unchanged (one key opens every unit, orphan clips included).
- A trailing partial aligned unit that is inside a mapped range AND flagged
encrypted now fails loud (a CBC fragment split across a boundary cannot be
decrypted) instead of being emitted as ciphertext-as-clear.
- CSS DVDs no longer mux to garbage. Every DVD read path — the file-backed mux
highway (`build_iso_pipeline`) and the live-drive single-pass `DiscStream`
now resolves the per-VTS title key at read time through one shared step
(`resolve_dvd_title_key`), cracked keylessly in playback order from the title's
own extents. An uncrackable title hard-fails (E7023) instead of passing
scrambled sectors through as plaintext; `--raw` skips the crack entirely; a
user Stop mid-crack surfaces as `Halted`.
### Changed
- DVD scan no longer cracks a title key up front (the key is per-VTS, so a single
disc key was meaningless). Scan does only the CSS bus-auth read-unlock — hoisted
before the UDF prefetch so scrambled small/menu VOBs no longer cost a rejected
read each. Cuts a CSS-DVD scan from ~25s to ~6s.
- Unlocker report: the DVD entry is renamed `CSS``DVD`.
## [1.5.1] — 2026-07-20
### Fixed
- **TrueHD audio is no longer silently dropped (and no longer sends decoders out
of memory).** The previous release added an MLP major-sync checksum gate to drop
genuinely-undecodable audio frames, but the checksum was computed with mismatched
byte order — the 16-bit CRC result was folded in one endianness and compared in
the other — so it never validated a real major sync. The parser then judged every
major sync corrupt and dropped every audio frame from the first one onward,
flushing the whole TrueHD track at the end of the file: de-interleaved from the
video, which sent players and integrity checkers into an unbounded memory spiral
("decoder ran out of memory"). The checksum now matches the reference
implementation byte-exact (cross-verified against real 7.1/Atmos and 5.1 discs),
so major syncs validate and only genuinely-corrupt frames are dropped; as a
safety net, a major sync the parser still can't validate is kept rather than
allowed to drop an entire track. TrueHD titles produced after the checksum gate
landed need a re-rip.
- **HD DVD AACS key files are now found on every disc, not just the common
layout.** The AACS directory and title-key filename on HD DVD are chosen by
the authoring house, and freemkv previously assumed one fixed spelling
(`/ANY!/VTKF000.AACS`). Discs that name their AACS directory differently (e.g.
`AAC!` instead of `ANY!`) or ship numbered title-key files (`VTKF090.AACS` /
`VTKF100.AACS` rather than `VTKF000.AACS`) are now handled: the AACS directory
is located by its contents and every title-key file in it is picked up. Blu-ray
and UHD are unaffected.
- **HD DVD multi-title decryption reads the right keys.** The HD DVD title-key
file (`VTKF*.AACS`) stores its keys in 36-byte records — per the AACS HD DVD
specification, and confirmed byte-exact on real discs. freemkv had been reading
them at a 32-byte stride, which lands the first key correctly but drifts off
every key after it, so only single-title discs decrypted. Discs with more than
one protected title now recover every title's key instead of only the first.
(Choosing the correct title-key file when a disc carries several playlists
still needs verification against an encrypted HD DVD.)
- **A dirty disc can no longer "rip clean" but decode with errors.** freemkv now
asks the drive to *report* marginal reads instead of silently returning
best-effort data as success — on smudged/scratched media a drive can hand back
subtly-wrong bytes with a clean status, which used to slip through the rip and
surface only as playback/decode errors. A read the drive had to fight for is
now distrusted and re-read in the patch pass: a clean re-read wins, and a spot
that's genuinely unreadable becomes an honest gap rather than silently-wrong
data. Best-effort per drive, and it changes nothing on a clean disc.
## [1.5.0] — 2026-07-19
### Added
- **MP4 as a source (`mp4://`)** — read a progressive `.mp4` back in and send it to
any sink (`mp4:// mkv://`, `mp4:// audio://`, `mp4:// json://`, …). The round-trip
is frame-exact.
- **Native MP4 output (`mp4://`)** — a disc goes straight to a play-everywhere
`.mp4` in one decrypt pass, no external transcoder. Carries HEVC / H.264 video (with HDR10) and
AC-3, E-AC-3, and DTS / DTS-HD audio, and is faststart by default so it plays over
HTTP without downloading the end first. It's a **compatibility export, not
archival**: MP4 can't hold TrueHD, LPCM, or bitmap (PGS / VobSub) subtitles, so
those are **excluded with a loud, itemized report — never a silent drop**
(`mkv://` stays the keep-everything path).
- **Five extraction sinks — dissect a title, don't just rip it.** New destinations
that pull one part of a title out on its own:
- **`video://dir/`** — each video track to its own native elementary-stream file.
- **`audio://dir/`** — each audio track to its own file in its native container
(`.thd`, `.dts`, `.ac3`, `.eac3`, `.aac`, `.flac`; LPCM as `.pcm`).
- **`sub://dir/`** — each subtitle track to its own file (PGS `.sup`, VobSub
`.idx` + `.sub`, text `.srt`).
- **`chapters://file`** — a title's chapter markers as a sidecar (`.xml` / `.txt`
/ `.ogm` / `.vtt`).
- **`json://file`** — a title's complete structure as JSON.
`chapters://` and `json://` read nothing of the elementary streams, so they
return in seconds.
- **Damaged audio frames are dropped instead of shipped as glitches.** When a
source disc has a corrupt audio frame, freemkv drops that frame rather than muxing
it as a decoder-choking glitch — keeping A/V in sync (a drop is a silence gap,
never a shift) and logging every drop. Works across DTS, AC-3 / E-AC-3, FLAC,
MP2 / MP3, AAC, and TrueHD, each using the format's own integrity check; a track
that is mostly undecodable is dropped whole. This catches structurally-broken
frames — corruption of the audio *data* inside an otherwise-valid frame is source
damage that can't be told from good data without decoding.
- **Forced subtitles detected from the stream.** A PGS subtitle track is flagged
forced when its content is entirely forced/narrative subtitles, read from the
stream itself rather than the disc's metadata — so it works on discs that carry
none. `info -v` reports the same, so `info` and a rip agree.
### Changed
- **`json://` emits the complete title model** — every field the scan resolved:
video resolution / frame rate / HDR / colour, audio channel layout / sample rate /
language / purpose, the subtitle forced flag, plus the clip list and chapter names.
### Fixed
- **TrueHD: a couple of transient errors no longer discard a whole track.** A
corrupt access unit is dropped forward to the next clean sync point, but a short
burst of damage no longer trips the whole-track drop, and a corrupt sync point can
no longer shift the audio that follows.
- **Free-format MP2 / MP3** is a legal, decodable mode and is no longer dropped.
## [1.4.5] — 2026-07-18
### Fixed
- **FMTS (AACS 2.1) forensic discs now mux to a clean, single-variant stream.** A
forensic segment interleaves the local device group's variant with a foreign
group's at the aligned-unit level. The mux decrypted only our half but left the
foreign half in the buffer as ciphertext, on the assumption that the demuxer
"drops untouched ciphertext cleanly." It does not — a foreign unit's bytes hit a
tracked PID at the 192-byte stride, mis-parse, and trip the demux's concealed-gap
keyframe resync, which discards good frames of ours around every segment (visible
playback glitches). `AacsKeyMap::read_plan` now turns the map into the title's
read plan: every default / CPS unit, plus inside a forensic segment **only our
phase's units**. The foreign half is never read, decrypted, or handed to the
demux. On a retail 4K UHD title this took concealed-gap resyncs from **349 → 0**
and recovered ~2 GB of previously-dropped frames. Wired into **both** mux paths —
the file-backed highway (`build_iso_pipeline`) and the inline live-drive
`DiscStream` (`with_key_map`) — so single- and multi-pass FMTS rips are both clean.
### Changed
- **Key-bearing types redact their `Debug` output.** Every type that carries key
material (device keys, processing keys, unit keys, media keys, VUKs, resolved
chains, CSS/AACS state, …) now prints a `<redacted>` marker instead of the bytes,
so no key can reach a log or panic message. Each is covered by a test asserting no
key byte appears.
- **Hex parsing is centralized and case-insensitive.** A single set of canonical
`0x`/`0X`-tolerant hex→integer parsers replaces scattered ad-hoc parsing (this is
what silently dropped keydb device keys written with an uppercase `0X` prefix).
- **Internal-only public surface narrowed to `pub(crate)`, and duplicate
`foo_with_X` methods collapsed to one** — no behavioral change, smaller API.
## [1.4.4] — 2026-07-17
### Fixed
- **Online key requests are no longer silently dropped on discs that yield few
sample units.** The online key source refuses any request carrying fewer than
`MIN_SAMPLE_UNITS` (8) encrypted-content samples — too few can match an
incidental unit rather than the one asked about (a false positive, most acute on
AACS 2.1 forensic-variant content). autorip gathered only 4, so every online
lookup was skipped before it ever reached the key service and surfaced to the
user as "key service down." autorip's sample count is now tied to
`MIN_SAMPLE_UNITS` with a **compile-time floor**, so it can never regress below
the minimum again.
### Changed
- **The online request is assembled from a proven-sufficient sample set.** New
`DecodeSampleSet` (`libfreemkv::keysource`) wraps the content-unit samples and
can only be constructed with at least `MIN_SAMPLE_UNITS` of them — so an online
key request cannot be built from too few samples. The minimum is validated once,
at construction, rather than by a runtime check a caller could forget.
## [1.4.3] — 2026-07-17
### Changed
- **`MIN_SAMPLE_UNITS` moved to the base crate.** The minimum sample count an
online key request must carry now has a single definition in
`libfreemkv::keysource`; `freemkv-keysources` re-exports it, so the online source
and libfreemkv's own forensic query size their requests from one shared value.
- **The online unit-key reply is parsed as a list.** A response carries either a
single Unit Key (ordinary disc) or the full ordered set (an AACS 2.1
forensic-variant disc); the client accepts both and maps array position to
forensic index.
### Added
- **Forensic-variant online query samples the anchor segment.** On an AACS 2.1
forensic-variant disc the online key query draws its sample from the first
forensic segment (index 1) — one canonical, deterministic sample — instead of an
arbitrary segment.
## [1.4.2] — 2026-07-15
### Fixed
- **Mux no longer nulls decryptable video or storms the key server on a
bad-encoded region.** 1.4.1 relaxed the decrypt gate but left the surrounding
machinery in place. On a unit whose key *decrypted* but whose plaintext didn't
reassemble to clean MPEG-TS, the read path still restored ciphertext, tallied
loss, and re-asked the online key server (forever returning the same correct key)
while the mux concealed the unit as NULL TS. The root cause: *"did a key produce
clean TS?"* was used as the verdict *"did we decrypt?"* — they are not the same.
A correct key can decrypt content with broken encoding; broken TS is a muxer
concern, never a decrypt verdict.
### Changed
- **One decrypt authority; policy at the caller.** `decrypt_sectors` is now a
pure decrypt: applies the CPS unit key in place, leaves plaintext, and reports
unverified bytes. It never restores ciphertext, nulls, or re-fetches a key.
Clean-TS status is only a key-*selection* hint (multi-CPS) or a read-*verify*
signal (sweep/patch). Callers own the policy: the mux passes decrypted bytes
through unconditionally (the demuxer handles bad TS); sweep/patch treat an
unverified unit as a failed read and re-read it. Removes the decrypt-time
ciphertext restore, the mux NULL-TS conceal loop, and the per-unit key-server
refetch, plus the dead `aacs_unit_still_ciphertext` predicate.
- **Decrypt and TS-structure are now separate primitives.** AACS has no MAC;
the only "did it decrypt?" signal is whether plaintext looks like MPEG-TS —
a data-quality / key-selection question, not a decrypt verdict. The old
`decrypt_unit(...) -> bool` is split into `decrypt_unit_raw` (pure crypto) and
`is_clean_ts` (structural check), composed explicitly only where needed. The
mux calls only `decrypt_unit_raw`.
- **Key-proof floor replaces the 75% supermajority.** The old proportion
(≥75% of content packets synced) conflated *the key worked* with *the content
is well-encoded*. `is_clean_ts` now requires `synced >= min(E, 4)` on
**encrypted** packets (skipping packet 0 whose `0x47` is in the clear seed):
four synced packets ≈ 1-in-4-billion false-positive; `min(E, 4)` scales to
short fragment tails so they're never false-rejected. A unit is "opened" when
a handful of packets prove the key — bad-encoded packets are the muxer's job.
## [1.4.1] — 2026-07-14
### Fixed
- **Mux no longer discards good video over a single defective packet.** AACS
decryption required **every** content packet to be conformant MPEG-TS: one
authored-bad packet (encoding defect, AACS 2.1 forensic-variant frame) made
the mux conceal the **whole** 6144-byte aligned unit as NULL TS (up to 31/32
good packets discarded, tallied as loss). On affected discs this produced
false "corruption" over otherwise-perfect video (~466 MB concealed across two
UHD titles). The gate is now a padding-aware **≥75% supermajority** of content
packets restoring their `0x47` sync — no wrong key reaches this threshold
(uniform-AES noise floor ≈ 256⁻ⁿ), but a minority of authored-bad packets
still passes. Opened units flow through verbatim; the demuxer drops
non-conforming packets on sync-loss. TS-sync conformance is a muxer concern,
never a decrypt verdict. (The supermajority threshold is tightened in 1.4.2.)
- **MVC (Blu-ray 3D) track signals unified and hardened.** The `mvcC`
`CodecPrivate` extension, the `BlockAdditionMapping`, and each frame's
`BlockAdditional` now all derive from a single `MVCDecoderConfigurationRecord`
built once per track, so they can no longer diverge. A track is flagged 3D
only when that record actually builds — a malformed dependent-view parameter
set no longer emits a mapping with no matching record (previously the flag was
taken from `mvc_params.is_some()`, which could orphan a `BlockAddID`). The base
track's `CodecPrivate` now carries the `mvcC` extension block
(`avcC ‖ u32be(size) ‖ "mvcC" ‖ record`, Matroska-spec size = block 4) so
players and mediainfo detect MVC at the track level.
## [1.4.0] — 2026-07-13
### Added
- **Blu-ray 3D (MVC) support.** A 3D disc now rips to an MKV that preserves
**both eyes** as a single MVC video track — the AVC base (left) view in each
Block, and the MVC dependent (right-eye) view as a per-frame `BlockAdditional`
under an `mvcC` `BlockAdditionMapping` (`MVCDecoderConfigurationRecord` per
ISO/IEC 14496-15 §7.6.2), paired to the base by PTS. Remux only — no
transcode, no side-by-side conversion. The Blu-ray scan reads the interleaved
`STREAM/SSIF/<clip>.ssif`, enumerates the dependent view (stream_type `0x20`)
by the BD-3D PID convention, and parses it in a parameter-set-passthrough mode
so every dependent frame is a self-contained access unit. Verified on
*300: Rise of an Empire*: one MVC track, ~8.7 GB dependent payload carried in
per-frame BlockAdditionals, base view byte-identical to the 2D rip.
## [1.3.2] — 2026-07-10
### Added
- **AACS 2.1 (FMTS) variant-decode foundation.** `UnitKey` gains a
`variant_number` field (`0` = ordinary content, `1..=32` = a forensic
variant) with `UnitKey::new` / `UnitKey::variant` constructors, and a new
`aacs::variant_select` module resolves a disc's single forensic variant and
classifies each aligned unit — decrypt with the default key, decrypt with the
variant key, drop a foreign variant, or conceal a keyless forensic unit. This
is the groundwork for selecting one variant's segments and dropping the other
31; the decrypt-pipeline wiring lands with the variant key source.
### Fixed
- **`IndividualSegment.tbl`: the per-record field is the variant, not a segment
number.** `Segment.number``Segment.variant`. Verified against a retail 2.1
disc, the field cycles `1..=32` across the table (a per-variant tag) rather
than counting up, so variant selection routes on the correct value.
## [1.3.1] — 2026-07-10
### Licensing
- **Relicensed to the MIT License, from 1.3.1 onwards** (releases up to and
including 1.3.0 remain under AGPL-3.0).
### Added
- **Authoritative HD-DVD title composition** from the Advanced-Content playlist
(`ADV_OBJ/VPLST000.XPL`): each title's clips, real duration, display name, and
chapters come from the disc's own playlist instead of a clip-name heuristic. A
layer-break split (`FEATURE_1`+`FEATURE_2`, `feature`/`feature_Divide`) composes
into ONE title with the two parts as clips and their title-time offsets. Falls
back to the clip-name heuristic when no playlist is present.
## [1.3.0] — 2026-07-08
### Added
- **AACS 2.1 (FMTS) is a first-class disc format.** FMTS discs are detected,
labeled, and scanned as their own format rather than misread as plain UHD. The
forensic variant segments are located from `IndividualSegment.tbl` and the
`SegmentKey.tbl` container is parsed; the bulk of the title decrypts with the
unit key as usual, and the forensic segments (for which no segment-key source
exists yet) are skipped as expected loss, so a 2.1 disc rips mostly-complete
instead of failing outright.
- **AACS 2.1 variant Media Key chain runs end to end.** The variant media key is
derived as a clean Processing-Key to media-key primitive, with the record
layout pinned against reference variant MKBs — the per-slot `C` block from the
`0x0c` cvalue table, the `VARIANTS` table plus trailing nonce at `0x2d`, and
`VKD` at `0x2f` — so a genuine variant MKB resolves through the ladder.
- **Partial HD-DVD support.** HD-DVD is detected as its own format and its
`HVDVD_TS` `.evo` clips mux through the pipeline: EVO video is demuxed from the
MPEG program stream, including VC-1 titles carried on extended stream id `0xFD`
(real selector in the PES `stream_id_extension`), with the VC-1 access units
reframed so each I-frame keeps its preceding sequence and entry-point headers.
Title composition is heuristic for now (authoritative program-chain parsing is
planned), so a disc that authors two distinct features under the layer-break
naming may present them as one title.
- **Display-order timestamps for program-stream H.264 / VC-1 / HEVC.** A program
stream stamps a PES PTS only once per GOP; the parsers now reconstruct a
display-order PTS per frame from the coded picture type and the sparse anchor
(duration self-calibrated from anchor spacing), so a decoder no longer sees
colliding DTS. Gated to the program-stream path — the BD/UHD transport path
(per-frame PTS) is unchanged.
- **Stream-label parsers: reader-backed detection and a menu-language fallback.**
Label detection can inspect a jar's contents, so vendor parsers claim only
their own discs; a new last-resort parser reads menu-artwork languages.
- **keydb round-trips AACS 2.0 host certs** (the `HC2` line) so a load/save cycle
no longer drops v2 host credentials.
### Changed
- **MPEG-2 reassembles through the shared `AuAssembler`.** The MPEG-2 parser's
hand-rolled PES buffer and offset-keyed mark queues are replaced by the same
access-unit assembler the H.264/HEVC/VC-1 parsers use (in a new MPEG-2 mode);
the GOP-buffered `temporal_reference` reorder and PTS origin-locking are
unchanged, so DVD output is identical.
- **Generic per-scheme recovery seam.** Decrypt-miss handling is now a
scheme-neutral seam the input stream installs (no recovery, or an AACS
fresh-key fetch), with CSS self-recovering separately from the data itself. An
undecryptable unit is counted the same whatever the scheme, so the separate
"undecryptable" loss bucket folds into one loss count.
- **`aacs` module reorganized.** The former god-module is split into
`media_key` / `volume_key` / `inf` / `resolve` / `mkb` / `crypto` / `content`,
the `boil` veneer is removed, and module paths (not a `mod.rs` facade) are the
public API.
### Fixed
- **Main title is chosen by largest physical size, not clip count**, so a
chapter-per-clip disc (e.g. Fast & Furious) is no longer mis-ranked behind a
virtual composite.
- **A fresh-rip ISO `sync_all` failure is no longer swallowed**: `is_regular` is
read from the open file handle instead of a pre-create `metadata(path)` that
always failed on a path that does not exist yet.
- **A transient CLIP-info parse failure no longer suppresses a clip's extents**
for a later playlist item that references the same clip.
- **Reverify downgrades that fail to persist are logged, not swallowed**, so a
bad unit cannot be silently mismarked good on resume.
- **The CLI sanitizes on-disc metadata** (title, volume label, playlist, stream
labels) before printing, so a crafted disc cannot inject terminal escape
sequences.
- **keydb entry validation matches the parser exactly** — a `0x` line counts only
with a ` = ` — so content that parses to zero usable entries can no longer be
saved as valid.
- **autorip** recovers a poisoned config lock in the rip thread instead of
panicking it, and corrects the resume pass count.
- Criterion stream numbering (a map value of 0 no longer shadows stream 1); AACS
resolve classifies a media-keys-only source missing the VID as "VID
unavailable"; a dropped partial PES flags a discontinuity; the no-demuxer path
detects an early consumer disconnect.
### Performance
- **Decrypt thread count is resolved once** and cached off the per-buffer hot
path (the env var and `available_parallelism` are no longer probed per call).
## [1.2.2] — 2026-07-04
### Added
- **AACS 2.1 Media Key Variant support.** The Media Key Variant scheme is now
detected and parsed from the real MKB record types found on variant discs —
`0x2d` (Encrypted Media Key Variant Data), `0x2f` (Variant Key Data table,
65,535 × 16), and `0x0c` (variant cvalues, one per subset-difference slot) —
replacing the earlier placeholder `0x82`/`0x83` types, which were a guess and
appear on no real MKB. The V2.0→V2.1 upgrade detection and fixtures are updated
accordingly, so a genuine AACS 2.1 variant disc now resolves.
- **`resolve_candidate`** — one composed, pure-derivation boil-down for a
candidate key at any ladder rung (DK/PK/MK/VUK → terminal unit keys), parsing
`Unit_Key_RO.inf` at the disc's declared AACS version and returning every CPS
unit key. Consumers stop re-composing the ladder; every client hardens a single
implementation.
### Fixed
- **`mk_from_dk` does the real Subset-Difference walk again.** It previously ran
the Media-Key-Variant path, which needed an integrator KCD absent in-tree and
errored for every real disc — effectively dead for both consumers. It now
performs the genuine device-key SD walk; the Volume ID enters at the VUK step
(where it belongs), not the MK step. This revives the DK→MK fallback across the
toolchain (`freemkv-keysources` adopts the corrected two-argument call).
- **autorip: a down online key service is no longer reported as a missing key.**
When the online key source resolves no key for an encrypted disc, autorip now
runs one bounded reachability probe (SSRF-pinned, ~8 s, no redirects) and
distinguishes a transient outage (transport error / 502·503·504 → down; 429 →
rate-limited) from a genuine no-key (any real HTTP answer → up). A transient
verdict triggers a bounded key-resolution retry (3 attempts, 8/16/32 s backoff)
and, if the service stays down, parks the disc in a distinct retryable state
("Key service unavailable — temporary outage, not a missing key; will retry.")
instead of the permanent "no keys found". Never hammers the drive or service.
### Performance
- **Processing-Key resolution is ~15× faster on UHD.** A Processing Key is the
key at its subset-difference node (one AES-G from the Media Key), so it is now
tried directly against the MKB cvalue tables (direct PK × cvalue iteration)
instead of BFS-walking the SD tree at unknown depth — which was both wrong for
terminal PKs and slow on a large UHD MKB (~181k cvalues). PK derivation on UHD
drops from ~37 s to ~2.4 s; the SD tree walk now lives solely in the device-key
path.
### autorip
- **Clear stuck move errors from the System tab.** Each move-queue error now has
a ✕ to dismiss it, plus Clear all and Refresh — so a resolved or stale error
can be cleared without restarting the container (the mover re-records any that
are still genuinely failing on its next tick).
## [1.2.1] — 2026-07-02
### Fixed
- **DVD DTS audio no longer muxes with non-monotonic timestamps.** A DVD
Program Stream packs several DTS core frames into one PES packet; the parser
stamped every access unit with that single PES timestamp and no per-frame
duration, so consecutive frames collided on one PTS and a strict decode/remux
a standard validator rejected the track — `non monotonically increasing dts to muxer`.
The DTS parser now derives each core frame's duration from its header
(`(NBLKS+1)*32` samples ÷ the `SFREQ` sample rate) and re-bases to each PES's
own container timestamp, advancing by a frame duration only *within* a single
PES — so the track stays monotonic and does not drift past its real length on
a feature-long title. The UHD DTS-HD MA path (one access unit per PES) is
unaffected: each unit keeps its own PES timestamp, preserving the 1.2.0 per-PES
attribution. Completes the DVD DTS fix begun in 1.2.0 (which corrected the
silent-track routing, exposing this timing bug). Note: genuinely corrupt
source DTS frames — valid framing, bad audio blocks — are passed through
faithfully; freemkv never fabricates or drops audio it can't prove is bad.
## [1.2.0] — 2026-07-01
### Breaking
The disc's AACS version is now carried through the key-resolution path as the
single source of truth for the `Unit_Key_RO` stride (AACS-1.0 = 48-byte,
AACS-2.x = 64-byte), so keys are always read at the disc's own layout. That
threaded one new value through three public signatures. In-tree consumers
(`freemkv`, `autorip`, `freemkv-keysources`) are updated; external callers must
adjust:
- **`DiscInputs` gains a `version: u8` field** (between `volume_id` and `mkb`).
Code constructing it with a struct literal must add the field. It is normally
obtained from `Disc::inputs()`, not constructed by hand.
- **`keysource::DiscInputsCtx::new` takes one argument, not two** — the version
is now read from `inputs.version` (`new(inputs)` instead of
`new(inputs, version)`).
- **`disc::read_aacs_inputs` / `read_aacs_inputs_from_drive` return a 3-tuple**
`(inf, mkb, version)` instead of `(inf, mkb)`.
- **`PassProgress` is no longer `Copy` and gains a `located: LocatedProgress`
field.** It now carries a `Vec` (the rendered bad-range drilldown), so it's
`Clone` only — still built once per throttled emission and passed by reference
to `Progress::report`. Struct-literal constructors must add the field (empty:
`located: Default::default()`). New public types `LocatedRange` /
`LocatedProgress`.
These are source-breaking for external crates.io consumers. Shipped under a
minor bump (1.2.0): libfreemkv's surface is not yet frozen and the only known
consumers are the in-tree toolchain crates.
### Added
- **Pass-N marginal-sector recovery specialists.** The patch pass gained a
roster of parameterized recovery techniques — read speed (max/min), cache
bypass (FUA), and traversal (linear fwd/rev, bisect, cache-prime, oscillate,
per-sector speed-sweep) — each targeting a distinct physical failure mode of
marginal media. A per-rip **decayed (EWMA) scorecard** grades every technique
by its recent recovery rate and re-orders them best-first, so the engine
hardcodes no conclusion: a technique that fits *this* disc floats to the front
and one that doesn't self-deprioritises (but is never dropped). Every read is
wedge-safe and deadline-bounded; the existing fast/deep recovery behavior is
unchanged (the specialists are additive, tried only on the hardened residue).
- **Opt-in flat-pool recovery scheduler (`FREEMKV_PATCH_FLAT`).** Collapses the
breadth-first recovery tiers into one flat pool so every technique gets a shot
at each bad range immediately, scorecard-ordered — a data-driven bandit for a
hardened residual (e.g. a late resume) where the tiered ladder would spend a
long time on cheap techniques before reaching the specialists. Unset keeps the
proven tier ladder as the default.
- **`PassProgress` is the complete, mapfile-free progress contract.** Every
emission now carries the fully-rendered "where is the damage" drilldown
(`located`): the bad ranges annotated with chapter + movie-time offset, the
main-feature at-risk time, the section count and the largest gap — computed by
the library from its in-memory mapfile + title. A client (autorip, a future
GUI/CLI) renders the disc map + at-risk time straight from it and never parses
the mapfile, so a mapfile→mapdb change is invisible to clients. Adds
`disc::locate_ranges`, the one-shot `disc::progress_snapshot_from_mapfile`
(builds a snapshot from a mapfile on disk so a boundary/verdict paint stays
mapfile-free client-side), and `consts::MILLIS_PER_SEC`.
- **`PatchOptions::fast_capture` — breadth-first patch recovery.** A fast-capture
pass reads each bad range once at the full batch and leaves every failed block
`NonTrimmed` for a later pass — no bisect, no re-read, no per-sector grind — so
a first retry pass grabs the readable blocks (a sweep's good skip-ahead
overshoot) of EVERY range before any single range's slow per-sector recovery.
No data is dropped: a failed block stays `NonTrimmed` (retried by a granular
pass), never `Unreadable`. A transport fault still aborts. `Disc::copy`'s
internal patch leaves it `false` (single-call full recovery).
- **Mux loss concealment — a logged gap still produces a decode-clean file.**
When a unit genuinely cannot be decrypted on the mux read path (a key the disc
never yielded, after the rip's own decrypt-verify already failed loud and
re-read), the mux no longer passes ciphertext downstream or emits a broken
frame. The undecryptable aligned unit is concealed as NULL transport-stream
packets (PID 0x1FFF, invisible to every real stream), and the codec layer
**drops forward to the next keyframe** so no frame with a dangling reference
reaches the muxer. A deep validator scan of the result is clean — no missing
references, no partial frames. The loss is tallied and logged, never silently
dropped, and the mux always completes. Audio and subtitle tracks have no
cross-frame references, so only the directly-affected frames are dropped there.
Decrypt-verify remains a **rip** gate (fail loud → re-read), never a mux gate.
- **`Disc::unlocker_matrix()` — registry-driven unlocker did-work report.** Returns
each registered unlocker's name alongside a `did_work` flag recording whether it
performed authentication steps during the current rip. Callers (autorip, the CLI)
surface this so an operator can confirm at a glance which unlock paths —
LibreDrive firmware, AACS, CSS — actually ran, with no hardcoded names on the
caller side.
### Changed
- **One hex parser.** All hex parsing (keys, IDs, key-source inputs) routes
through a single `libfreemkv::hex` parser instead of several ad-hoc decoders,
so length/odd-nibble/invalid-digit handling is identical everywhere.
- **Robust encrypted-unit sampling + a single MKB framing walker.** Up-front
AACS sampling tolerates content layouts that previously yielded too few
encrypted units to resolve a key, and the Media Key Block is now walked by one
framing routine shared across the in-band and out-of-band readers (no
divergent record-stride logic). AACS resolution hardened around these paths.
- **One reader, one `DiscInputs`.** `Disc::inputs()` is now the single, complete
source of a disc's AACS inputs (inf, MKB, VID, disc_hash, version), and
`read_aacs_inputs*` returns the version alongside inf+MKB. Both the CLI and
autorip resolve through `Disc::inputs()`; the duplicate out-of-band readers
(autorip's `key_files()`/`volume_id()`) and the stale mapfile-VID read are
removed. AACS file paths and the AACS major versions are now named constants
(`aacs::PATH_*`, `aacs::AACS_MAJOR_*`, `AacsVersion::major`/`from_major`) so a
fallback or stride change lives in exactly one place.
- **Pass-N recovery rebuilt as a bounded, never-hang handler chain.** The 1.1.0
patch loop retried each bad range sector-by-sector until a per-range budget was
exhausted, with no escape from a wedged drive short of the watchdog firing after
tens of minutes. 1.2.0 replaces that with a two-tier handler chain dispatched
breadth-first, largest bad range first:
- **Jump** (lead tier): reads each range in large forward-skipping batches to
quickly locate readable islands — clearing a multi-gigabyte dead spot in
seconds rather than sector-by-sector.
- **Bisect** (trailing tier): binary-searches the boundaries of each remaining
bad block, converging to within a single sector of the last-readable LBA.
Boundary-probe reads are exempt from the early-yield stall so the boundary
walk always completes.
- **Handler scorecard**: handlers that make progress stay at the front of the
rotation per rip; an idle handler is ranked last so proven performers lead.
- **Wedge detection**: a pass-level streak counter tracks consecutive
wedge-family senses (HARDWARE ERROR / ILLEGAL REQUEST) across section
boundaries. At the threshold the pass aborts and a soft un-wedge
(`Drive::spin_cycle()` — START STOP UNIT, no eject) runs before the next retry
pass, instead of grinding at near-zero throughput until the pass watchdog
fires.
No data is dropped: a block that neither handler recovers in a pass stays
`NonTrimmed` for the next pass.
### Fixed
- **DVD DTS/LPCM audio tracks no longer mux silent.** On DVD-Video the
`private_stream_1` sub-stream id's low nibble is the audio-stream *number*
(shared across codecs), not a per-codec ordinal. A DTS or LPCM track that
wasn't the disc's first audio stream got a sub-id one too low, so the demux
routing key (`0xBD00 | sub_id`) never matched and every packet was dropped —
the track appeared in the container but played silent (AC-3 at position 0
worked by coincidence). Audio sub-stream ids are now assigned by positional
stream number, so a DTS 5.0 track after an AC-3 5.1 track routes correctly.
- **ISO mux no longer drops real video at content-fragment tails.** A title's
encrypted content can end mid-AACS-unit, with the disc zero-padding the rest
of the 6144-byte aligned unit to the next fragment. The decrypt-verify
demanded the TS sync byte on *all 32* source packets, so it rejected such a
tail unit over its legitimate padding — discarding the real video packets it
contained. On a flawless rip this surfaced as a small phantom "loss" at mux
(and, once retries were exhausted, a truncated MKV). Unit acceptance is now
**padding-aware**: only packets whose *source* (pre-decrypt) bytes are
non-zero must restore their TS sync; the zero padding is excluded from the
check and emitted as clean zeros. A full content unit still requires all 32
(unchanged — no wrong-key relaxation), and a unit whose *non-zero* tail fails
to decrypt is still rejected as a genuine bad read.
- **ISO online key resolution now sends the Media Key Block.** Capturing a
disc's AACS inputs at scan read the MKB with a full `read_file` of the
~128 MiB `MKB_RO`/`MKB_RW` allocation, which fails on file-backed readers —
leaving the MKB empty, so `Disc::inputs()` shipped `mkb=0` to an online key
service and the request was rejected (no key → no decrypt). Scan now reads the
MKB through the same bounded prefix-grow + trim reader as the out-of-band
path, so `Disc::inputs()` is the single complete source of AACS inputs — one
reader for every caller.
- **Read-time key-fetch parses `Unit_Key_RO.inf` at the disc's own AACS stride.**
The on-demand fetch (for a CPS unit not sampled up front) hardcoded the V20
64-byte stride, so an AACS-1.0 (V10) disc whose key arrived as a VUK derived
the wrong unit keys. `DiscInputs` now carries the disc's `version`, and the
context parses at the matching stride — the disc is the single source of truth
for its own stride (no separate version argument to drift).
- **A dry key-fetch for one unit no longer blocks fetching a different unit.**
A global "fetch spent" latch meant that once the key service returned nothing
for one CPS unit's ciphertext, no further unit was ever asked — so a multi-CPS
disc could strand a unit whose key the service *would* have served. Replaced
with a per-unit "already-asked-dry" set (still bounded by the fetch budget).
- **`verify::push_ranges` uses saturating arithmetic** so a corrupt-disc LBA near
`u32::MAX` can't panic (matches `udf::merge_ranges`).
- **Audio no longer corrupts at a stream discontinuity.** At a transport-stream
discontinuity — a continuity-counter break, an adaptation-field
discontinuity_indicator, or a concealed-loss gap — the AC-3 / DTS / TrueHD
parsers held a *truncated* partial access unit and spliced the post-gap bytes
onto it, manufacturing a corrupt frame (a validator reports "exponent out of range" /
"Failed to decode block code(s)" / "Invalid data found") and, for TrueHD, a
non-monotonic timestamp band on multi-segment titles. The video path already
resynced via the keyframe gate; the audio parsers now do too — on a
discontinuity they drop the un-completable partial and resync on the next
syncword, rebasing the timestamp from the post-gap PES. A discontinuity becomes
a clean single-frame gap instead of a corrupt splice. Audio has no inter-frame
references, so dropping the truncated partial is the complete fix; the approach
matches how mainstream transport-stream demuxers behave.
- **Drive-prep firmware unlock skipped for DVD discs.** An
`if disc_is_dvd() { return }` guard in `Drive::init()` (present since
1.0.0-rc.1) bypassed the entire drive-prep unlock step for DVDs. That unlock is
what removes riplock and raises the drive to maximum read speed — a drive-level,
disc-independent feature — so every DVD rip ran at riplock speed (~0.4× rated,
multi-hour ETA). The guard is removed; all disc types now go through the full
drive-prep sequence. UHD and Blu-ray were unaffected (they already ran through
the unlock path).
## [1.1.0]
### Added
- **Post-read decrypt-verify gate.** Every AACS unit read off the disc is now
buffered, re-aligned to its clip-file 6144-byte unit grid, and verified
(CPI flag → decrypt → strict all-32 TS-sync)
before it is signed off as good. A unit that no held or freshly-fetched key
decrypts is treated exactly like a bad read — re-read by
the patch pass, terminal loss only if truly unrecoverable — closing the
"silent bad read" class where a sector reads OK but its ciphertext is subtly
wrong. **Fail-safe:** it only ever downgrades a unit it is *confident* is bad;
every uncertainty (no keys, a merely-missing key, an unread/zero-filled sector,
a non-AACS disc) leaves the read byte-for-byte as before. Gated by a
compile-time kill-switch (`POST_READ_VERIFY`), and container-pluggable (BD/UHD
transport stream today, with an HD-DVD program-stream seam in place).
- **Every error is now `Error: E<code> <message>`, with an Error Codes
reference.** User-facing errors show their code so you can look it up, and a
new **Error Codes** page lists every code with its message, cause, and next
steps. A contract test guarantees every error variant has a code, a message in
all seven languages, and a Codes-page entry. Messages are source-agnostic
("key source", never a specific database).
### Changed
- **AACS decrypt acceptance is now standards-strict.** A key is accepted only
when the decrypted unit has the TS sync byte on *all* 32 source packets
(all-32 TS-sync verify), replacing a majority-vote heuristic where a wrong key
could coincidentally restore enough syncs to pass and silently corrupt a unit.
- keydb download/save moved out of the library into freemkv-keysources;
libfreemkv no longer has any keydb I/O (it already held no keys).
### Fixed
- **AACS content-certificate bus-encryption flag read from the wrong bit.** The
flag is bit 7 of byte 1 (`p[1] >> 7`) but was read as bit 0, so a
bus-encrypted disc parsed as *not* bus-encrypted — defeating the fail-loud
guard that refuses to decrypt bus-wrapped data to garbage when no bus key was
obtained. Also corrected the cc_id offset (byte 14) and the AACS2 type marker
(`0x10`). Confirmed against real retail content certificates.
- **DVD rips now start on the movie, not the disc menu.** A VTS title VOB's
start sector was read from the IFO as a VTS-relative pointer but used as an
absolute disc address, so a DVD title's read extents began `ifo_lba` sectors
too early — the rip opened on the disc's menu / VMGI region and only drifted
into the feature minutes later (Silence of the Lambs, for example, showed
several minutes of the main menu before the movie). The title VOB is now
rebased to its absolute on-disc location, so the rip begins at the first frame
of the feature. Aspect ratio and chapter timing were already correct; only the
starting sector was wrong. (Covered by a new absolute-placement regression
test.)
- **Container metadata correctness.** Unknown colorimetry now emits the CICP
"unspecified" code point (2) consistently across the MKV track and the FVI
sidecar (previously 0); PGS subtitle wipes use the NORMAL composition state
rather than a full epoch reset; and FVI source-byte offsets are written
within-sector per the format spec.
- **Multi-extent AACS alignment in `dir://` extraction.** AACS encrypts in
aligned units of 3 sectors (6 KiB), and the decrypt-on-read gate accepts a read
only when its LBA is unit-aligned against a base. The `dir://` file-tree
extractor set that base once, to the file's first extent. A fragmented file
(Long-AD / continuation-ICB allocation) has later extents starting at arbitrary
LBAs whose distance from the first extent is generally not a multiple of 3
sectors, so the first read of every later extent failed the gate, returned a
decrypt error, and the whole extent was written as a zero-filled hole — even
though the sectors were readable. The unit base is now re-anchored per extent
(matching the mux read paths), so each extent gates on its own unit grid.
Decryption math is unchanged. Same class as the rc.5.2 clip-anchor fix.
- **Distinct "no key" reasons.** When AACS key resolution has usable material
(device or processing keys) but cannot obtain the disc's Volume ID — needed to
derive the unit key — freemkv now reports a distinct "AACS Volume ID
unavailable" error (E7017) instead of collapsing it into the generic "no key"
error (E7022), which is now reserved for a genuine absence of any key material.
No key derivation or descramble logic changed — only the reason reported on a
resolution failure.
- **autorip keydb writes go to the right path.** Auto-download, daily refresh,
the "Update KEYDB" button, and the startup existence-check now resolve to the
service's config path (matching where reads look); they previously used the
CLI's executable-local default.
- **Crash-safety hardening** in `dir://` extraction and keydb writes (fsync of
files and parent directories around rename).
- **Windows-reserved filenames** (`CON`, `NUL`, `COM1`…) inside a disc's file
tree are safely renamed on extraction instead of aborting the walk.
- **`--version` now matches the build stamped into MKVs.** The CLI's `--version`
string and the `MuxingApp` / `WritingApp` fields written into every MKV now
derive from a single libfreemkv constant — the package version plus the git
short hash (e.g. `freemkv 1.1.0 (g835cc99)`). The muxer previously kept
its own copy of that string, so the two could drift; a binary and the files it
produces can no longer report different versions.
- **DTS-HD Master Audio: a false core-sync inside the lossless extension no
longer splits an audio frame.** A byte pattern in the extension substream that
resembled the `0x7FFE8001` core sync word could truncate the lossless
extension and produce decode errors on the affected frames. The extension
substream is now sized exactly from its header, so that pattern is skipped as
data.
- **TrueHD: decode timestamps no longer step backward.** In a case where the
source PES timing lagged the audio access-unit cadence, the muxed decode
timestamp could regress (non-monotonic-DTS warnings to the muxer); the running
timestamp is now clamped so it never goes backward.
### Tests
- 58 new tests across the toolchain (AACS key resolution, the unlocker seam, the
key sources, DVD/CSS, `dir://` routing, and autorip keydb resolution).
## [1.0.0-rc.5.3]
### Added
- **`dir://` output** — write a decrypted `VIDEO_TS` / `BDMV` file tree straight
from a disc or ISO instead of a single muxed file.
### Changed
- **Source-agnostic key errors** — decryption messages no longer assume a local
key database is *the* key source.
- **The default `keydb.cfg` location is next to the executable** (portable CLI);
the autorip service keeps its container path.
- **Simpler flags** — dropped `-k` (use `--keydb`) and removed `--device` (the
drive is named in the source URL, e.g. `disc:///dev/sgN`).
### Fixed
- **Fail loud on missing keys or bad input** instead of silently writing an
undecrypted file.
## [1.0.0-rc.5.2]
### Fixed
- **Reverted the rc.5.1 `DefaultDecodedFieldDuration` experiment for interlaced
SD-DVD.** rc.5.1 added a 20 ms `DefaultDecodedFieldDuration` field element to
the 576i/480i track header on the theory that Windows derives fps from it.
Captured evidence showed that element made Windows Explorer report 12.5 fps
(half) and MediaInfo flip the track to "Frame rate mode: Variable". The
element is optional in RFC 9559 and nothing requires it for interlaced SD, so
it is no longer written (`MkvTrack::video` now passes `field_duration_ns == 0`);
the track keeps `FlagInterlaced=1` + `FieldOrder=TFF` and the full-frame 40 ms
`DefaultDuration` (`1/DefaultDuration` = 25 fps), which is the frame rate the
source actually carries. How a given player or shell handler chooses to display
interlaced fps is not guaranteed.
- **Correct AC-3 audio track selected on DVDs with non-standard sub-stream
ordering.** freemkv assigned each declared audio stream a physical sub-stream
by ordinal (`0x80+n`), assuming the IFO's first stream lives at `0x80`. On
discs where the 5.1 main mix sits on a different sub-stream and `0x80` carries
a 2.0 down-mix (e.g. Silence of the Lambs), the 2.0 was muxed under a "5.1"
label. freemkv now probes each physical sub-stream's actual channel count from
the disc — scanning every AC-3 frame and taking the maximum, so a brief 2.0
logo bed at the feature head can't mask the real 5.1 — and routes each declared
stream onto the sub-stream that genuinely matches.
- **"Decryption failed" on large AACS Blu-ray titles fixed.** AACS encrypts in
aligned units of 3 sectors (6 KiB); the unit-alignment gate measured `lba % 3`
against absolute disc LBA 0, but the unit grid is actually anchored at each
clip's encrypted-region start. A clip whose start is not 3-sector-aligned had
its readable units wrongly rejected — failing the feature/large titles of some
discs while short clips passed. The gate is now clip-anchored.
- **Single-pass disc→MKV recovers marginal/transient sectors before failing.**
The direct-to-MKV path now gives the drive its full ECC recovery budget on a
bad sector (matching the multipass rip) instead of reporting a read failure a
multipass rip would have recovered.
- **4K decode glitches at non-seamless clip joins fixed.**
Titles assembled from clips joined at non-seamless boundaries no longer drop
reference frames at the join ("Could not find ref" stutter); the splice
keyframe is rewritten so the decoder discards only the genuinely-dangling
leading pictures.
### Changed
- **`freemkv-keysources` is now a pure key lookup.** The encrypted content-sample
reader and the candidate-key resolution loop moved into libfreemkv (they read
the disc and validate keys — decryption mechanism, not lookup). A key source
now only looks a key up and hands it back. Downstream API: use
`libfreemkv::read_encrypted_units` / `libfreemkv::resolve_and_apply` (was
`freemkv_keysources::read_sample_units` / `…::resolve_and_apply`).
### Added
- **`--log-level 3` is now self-sufficient for MKV/opening-frame diagnosis.**
The diagnostic pass now (a) dumps the ACTUAL MKV `TrackEntry` elements written
per track (`tag=mkv.track`: FlagInterlaced, FieldOrder, DefaultDuration,
DefaultDecodedFieldDuration via field-duration, Display dims, codecPrivate as
hex) so the Windows-fps-class metadata is verifiable from a log alone, and
(b) captures the first ~100 coded frames per track (raw bytes) to a
`<output>.opening.bin` side file with a per-frame summary line
(`tag=mkv.opening.frame`: track, key/delta, size, PTS) so opening-GOP / menu
issues are diagnosable from a future log without the disc. Both are gated to
log-level 3; a normal run opens no side file and records nothing.
### Verified
- **DVD opening-GOP / still-frame open handling is correct (no change needed).**
The hypothesis that the opening pictures get the wrong (last-seen) sequence
header or have their PTS floored to t=0 was traced and ruled out: the
codecPrivate is the FIRST sequence header (read once at headers-ready, before
any later AU), DVD VOBU structure guarantees each title opens on a sequence
header + I-frame (no mid-GOP open), the parser back-anchors leading
still-frames to the disc's real timeline, and the muxer anchors its timestamp
base on the opening keyframe's real PTS so the t=0 floor never corrupts it.
Regression tests pin all three.
## [1.0.0-rc.5.1]
### Fixed
- **CSS reads unlocked on enforcing drives.** CSS-protected DVDs on
drives that enforce CSS authentication previously produced an empty MKV
at exit 0, or hung indefinitely. The read path now issues the bus-auth
handshake (`css::auth::unlock_css_reads`) to unlock scrambled-sector
reads before attempting any data transfer, so the drive gates lift
correctly.
- **Keyless title-key recovery always runs.** The Stevenson known-plaintext
attack (`css::crack_key` / `src/css/stevenson.rs`) now recovers the
title key even when the bus-auth scan detects a CSS drive, removing a
code path that fell through to locked reads on certain disc/drive
combinations. A wrong key still fails cleanly (confirmed by a sector
descramble check) rather than producing silent garbage.
- **Early bail on undecryptable discs.** When CSS authentication succeeds
but no valid title key can be recovered, the mux path now terminates
with a clear error code instead of writing an empty (or zero-byte)
output file.
- **DVD audio channel count from AC-3 bitstream.** The audio channel count
is now parsed from the AC-3 elementary-stream bitfield rather than from
the IFO audio attributes, so the reported channel count always matches the
actual muxed audio even when the IFO attribute disagrees. Passthrough only
— no downmix is performed. (Selecting the correct audio sub-stream on discs
with non-standard ordering is a separate item — see Known issues.)
- **Interlaced MKV frame rate on Windows.** Interlaced content (576i/480i)
now emits a `DefaultDecodedFieldDuration` element in the MKV track
header, which Windows Media Foundation and Explorer use to derive the
display frame rate. Without it, players reported an incorrect or zero
frame rate on interlaced tracks.
- **Per-track `BPS` bitrate tags populated.** The `BPS` tag is written for
each track so players and shell extensions (Windows Explorer, MPC-HC,
etc.) can display the per-stream bitrate without reading the full file.
- **Interlaced field order corrected to TFF.** 576i tracks were written
with a bottom-field-first (BFF) container flag that disagreed with the
top-field-first order carried in the MPEG-2 stream; the MKV `FieldOrder`
element now matches the stream (TFF) so deinterlacers use the correct
field parity.
- **DVD first-play menu no longer prepended to the feature.** The title
VOBS base sector was read from the VTS menu-VOBS pointer (`vtsm_vobs`,
offset 0xC0) instead of the title-VOBS pointer (`vtstt_vobs`, 0xC4), so on
a disc that authors a per-title menu the entire menu VOB — e.g. a studio
first-play "the parental level has been set, press yes" prompt — was
prepended to the movie and every cell extent shifted back. The rip now
opens on the feature's first frame.
### Changed
- **AACS handshake skipped on DVDs.** The AACS authentication sequence is
no longer attempted on DVD discs (it never applied to CSS-encrypted
media); attempting it on a DVD drive was a no-op at best and surfaced
spurious errors at worst.
### Added
- **Structured disc diagnostics at `--log-level 3`.** A new diagnostic
pass emits structured log events at INFO level when the log level is 3
or higher: DVD PGC/cell layout and IFO video/audio attributes; BD/UHD
playlist, clip, and AACS metadata. Provides a single-command snapshot
for diagnosing mux or authentication issues without instrumenting the
source.
- **Reduced per-operation log spam.** Mux-read and seek operations are
demoted to TRACE (were DEBUG); benign navigation-packet drops are
summarized as a single counter at the end of the title rather than
logged per-packet.
### Known issues
- **Wrong audio track on discs with non-standard substream ordering.**
Audio sub-stream ids are assigned by per-codec ordinal rather than read
from the IFO/PGC stream-number table, so a disc whose physical substream
order diverges from the convention may select the wrong audio track
(e.g. a 2.0 stream in place of 5.1). Diagnose with
`freemkv info disc://… --log-level 3`; fix tracked for the next release.
## [1.0.0-rc.4.2]
### Fixed
- **Windows durability.** New platform-aware `io::fsync` module: directory
fsync is a no-op on Windows (std cannot open a directory there, which
logged a spurious warning on every mapfile write — including from the
CLI), and a shared `file_durable` helper opens files read+write before
`sync_all` so the flush succeeds on Windows, where `FlushFileBuffers`
rejects a read-only handle with `ERROR_ACCESS_DENIED`.
## [1.0.0-rc.4] — UNRELEASED
An audit-driven round of correctness, durability, and Windows-transport
fixes. No API changes; behavior is more conservative on damaged media and
on partial decryption.
### Fixed
- **Decrypt-time loss is accounted for.** A partial AACS/CSS decryption
failure can no longer pass as a perfect rip — skipped/undecryptable
bytes are folded into the loss total — and partial CPS-unit (per-title)
key coverage is rejected in the AACS validation gate instead of
producing partly-garbage output.
- **Durable writes.** `keydb.cfg` is written atomically (temp file +
fsync + rename), and the mapfile fsyncs its parent directory after the
rename so a resume checkpoint survives a crash.
- **Truthful error causes.** A server-dropped keydb download is
classified as a connection error, not a parse error; a missing home
directory maps to "not found" rather than a keydb-parse failure; the
I/O error from opening an AACS-inputs ISO is preserved; and a
transport failure is preserved through the AACS auth handshake instead
of being relabeled.
- A failed `READ CAPACITY` now warns instead of silently using a
zero-sector disc.
- A leaked pipeline consumer can no longer finalize an abandoned output.
- **Windows SCSI.** `ScsiPassThroughDirect` is packed to match the
`ntddscsi.h` layout, `StorageAdapterDescriptor.BusType` width is
corrected (`u8``u32`), oversized read batches on non-sysfs
(Windows) drives are bounded, `IOCTL_STORAGE_RESET_DEVICE` failures are
surfaced, and a device reset only sleeps on success.
- Mux now tracks skipped bytes so a partly-read title reports accurate
loss.
### Changed
- The per-read `Drive::read` trace event was demoted to TRACE so a debug
log isn't flooded by per-sector reads.
## [1.0.0-rc.2]
Second release candidate for 1.0. libfreemkv is the core library: disc scan,
multipass sector recovery, content decryption (CSS, AACS 1.0/2.0), and the
threaded mux pipeline that turns a disc or ISO into an MKV. This candidate adds
keyless DVD/CSS support and correct DVD video, on top of security and recovery
hardening.
### Added
- **Keyless DVD/CSS title-key recovery.** A CSS-protected DVD decrypts with no
key database — the title key is recovered directly from the scrambled disc
data via the Stevenson known-plaintext attack and
validated by descrambling a sector and confirming the known plaintext
reappears, so a wrong key fails cleanly instead of producing silent garbage
(`src/css/stevenson.rs`). `Disc::scan_image` recovers the same title key from
a raw, still-scrambled CSS ISO, so a raw image can be muxed without
pre-decryption.
- **MPEG-2 Program-Stream access-unit reassembler** (`src/mux/codec/mpeg2.rs`).
Buffers elementary-stream bytes across PES packets and emits exactly one
coded picture per MKV block, with presentation timestamps reconstructed from
the stream — fixing corrupted DVD video. Bounded buffer so a malformed stream
cannot exhaust memory.
### Changed
- Self-contained keyframes: the active param sets (HEVC VPS/SPS/PPS, H.264
SPS/PPS, VC-1 sequence/entry headers) are re-asserted at every keyframe and
any mid-title param-set change is emitted in-band, fixing whole-segment
HEVC/H.264/VC-1 corruption when a source stops repeating or reverts a param
set.
- Block timestamps use presentation order keyed on track type, so B-frame video
(including a Dolby Vision enhancement layer) keeps its true presentation
timestamps instead of decode-order timecodes.
- Mux unit alignment is scheme-aware (AACS vs CSS/none), so DVD extents are no
longer rejected for unit misalignment.
- MKV output records `freemkv <version>` in the Muxing/Writing application
fields, so every output file is traceable to its build.
- Subtitle `BlockDuration` values are scaled by the segment timecode scale, so
display durations are correct when the scale is not 1 ms.
- The NOT_READY retry pause in the patch (Pass N) loop is halt-responsive: a
stop request interrupts the drive-recovery wait immediately instead of
blocking shutdown.
- Bounded the keydb decompressed-plaintext reader (caps a malformed or
zip-bombed download).
### Fixed
- A `READ(10)` that returns GOOD status with a residual underrun is treated as a
failed read (routed to retry) instead of committing stale buffer data —
closing a silent-corruption hole in the sweep and patch paths.
- `raw_command` on Linux masks the `DRIVER_SENSE` bit before treating a result
as an error, preventing false transport errors on commands that return sense
alongside a GOOD response.
- `READ CAPACITY (10)` rejects the "capacity exceeds 32-bit" sentinel instead of
silently wrapping to 0 and misreporting disc size.
### Security
- Content keys (CSS disc/title keys, AACS unit/volume keys) are redacted in log
output (logged as `<redacted>` with a 1-byte fingerprint); a test guards
against any key field being logged with a raw value.
- The macOS SCSI shim uses `posix_spawn` directly instead of `system()` / `sh
-c`, eliminating a command-injection vector on the device-path string.
## [1.0.0-rc.1]
First release candidate for 1.0 — the first tagged 1.0 milestone of the core
library. Established the full feature set: multipass sector recovery, content
decryption (CSS, AACS 1.0/2.0) from `keydb.cfg`, disc parsing, and the threaded
mux pipeline (see "Pre-1.0 development" for the consolidated feature list).
## Pre-1.0 development
Versions 0.x were the iterative development series leading up to 1.0. The
highlights, condensed:
- **Multipass recovery engine.** Pass 1 sweeps the whole disc sequentially,
tolerating bad sectors with an adaptive damage-jump algorithm (mark the bad
range, keep going). Pass N retries the bad ranges with per-sector recovery
timeouts, reverse-direction reads, and range bisection. A mapfile tracks
per-sector state across passes so a rip can resume.
- **Drive and SCSI layer.** Single-shot, synchronous SG_IO transport on Linux
(with IOKit on macOS and SPTI on Windows), full SCSI sense decoding, and
drive enumeration / presence probes. Single-shot reads by design — recovery
lives in the multipass orchestration, not inline in the read path.
- **Content decryption.** CSS for DVDs and AACS 1.0/2.0 for Blu-ray and UHD,
with keys read from `keydb.cfg`. A single decrypting decorator wraps the
sector source so decryption is one audited surface, and a resolved key is
verified against disc content before it is applied.
- **Disc parsing.** UDF, MPLS/CLPI (Blu-ray), and IFO (DVD) parsing for title
and extent assembly, with bounds checks on values derived from untrusted disc
input. Canonical main-title selection picks the real feature over a
play-all virtual playlist on branching discs.
- **Mux pipeline (the "highway").** A three-stage threaded pipeline —
read+decrypt, demux, codec parse — with a recycled buffer pool, taking
file-backed mux from ~60 MB/s to several hundred MB/s warm-cache. Codec
parsers for HEVC, H.264, VC-1, MPEG-2, TrueHD, DTS(-HD), and PGS feed an
EBML/Matroska writer.
- **I/O stack.** Bounded-cache writeback (`sync_file_range` +
`posix_fadvise(DONTNEED)`) keeps the kernel dirty-page cache bounded on long
sequential writes, and time-batched mapfile persistence keeps NFS-staged rips
fast.
- **Library hygiene.** No user-facing English in the library — all errors are
numeric codes handled by the application layer. A large spec-grounded,
mutation-verified test suite guards the silent-corruption surfaces. Rust 2024
edition; release builds use thin LTO.