The dev branch tip must path-dep ../freemkv-unlock so CI's sibling checkout
(and local dev) resolves the branch tip; release.sh swaps it to a git tag
only in the tagged commit. dev had been left in the released git-tag
(v1.6.14) form after the last release, so the new 1.7.0 unlock API couldn't
resolve. Restore the path form the comment already describes.
Adopt freemkv-unlock's new Unlocker::unlock(). The bridge assembles the
firmware set (freemkv / LD / Renesas) for drive-prep and the disc set
(AACS cert + DVD) for bus removal, injecting host certs into the AACS
unlocker at construction — the loop no longer knows any unlocker's guts.
- run_features / run_bus now loop unlock() and return Ok(Option<Unlocked>);
drive init and do_handshake_cert adapt to the Option.
- unlocker_matrix reports the LD name and credits Renesas as a drive-level
(raw-read) route, so AACS shows "no" when any firmware route took the bus.
- Cert route sets HandshakeResult.drive_unlocked=false: the bus is removed
via read_data_key (AKE), not at the drive; the gate credits it that way.
Fixed
- mux/mp4: honour 64-bit largesize (size==1) and size==0 (run-to-end) in the
capped child-box scan; a largesize sibling previously ended the walk early and
later boxes vanished from a MOV/MP4.
Version 1.6.15 (unreleased) -> 1.7.0; changelog folded under 1.7.0.
Earlier 1.7.0 work remains as its own commits below this one: the
GHSA-j8ww-f5fg-9pmh sector-recycle soundness fix, CI/lint hardening (#15/#17),
docs, and Rob Ekl's scsi raw_command / fd-handoff change (#16).
raw_command clamped CDBs with cdb.len().min(16) instead of routing through
the shared checked_cdb_len guard, so an over-length or empty CDB became a
different command rather than a rejected one; it now returns the crate Result
so a rejected CDB is distinguishable from an I/O failure.
The recovery/Drop fd hand-off lacked a release edge: the teardown swap was not
a release op and the recovery CAS was not an acquire load, so a recovery thread
publishing its fd after Drop drained the slot could miss dead==true and leak the
descriptor. Both sides are now AcqRel, with the execute() drain left at Acquire
(sound via the release-sequence rule). A loom model (cfg(loom) dev-dep, its own
CI job) provides the regression coverage cargo test cannot.
Module docs for fd_handoff and timeline now resolve in their own scope, and item
docs stay within comment-guard's caps.
Fixes#16.
Declared rust-version and the ci.yml/qa.yml toolchain pins both sit at 1.88 (the
edition-2024 floor once zip 8 and let-chains are accounted for), so CI builds on
exactly the MSRV: an accidental bump to a newer toolchain fails CI by design and
moving the floor takes a deliberate pin edit. The uninlined_format_args allow
reconciles 1.88's clippy with the allow-by-default it became in 1.97+.
Correctness/robustness:
- scsi/macos_shim: guard g_handle / g_da_bsd read-modify-write with a
process-global mutex (scoped OUT of the 5 s async-claim wait) — closes the
DiskArbitration global-state TOCTOU on concurrent/re-entrant open+close.
- disc/profile: the public DiscProfile now surfaces `encrypted` and a numeric
`key_error` (Error::code of aacs/css failure) so a disc with failed key
resolution no longer serializes identically to a rippable one.
Performance:
- disc/dvd: read VIDEO_TS.IFO once and reuse it for both parse_vmg and
resolve_main_title (parse_vmg_with / resolve_main_title gain an optional
vmg_bytes) — eliminates a duplicate VMGI read on every DVD scan.
Tests:
- bdnav: MAX_OBJECTS count-cap rejection, MAX_STEPS non-convergence guard, and
an end-to-end index.bdmv + MovieObject.bdmv resolver test (all red-green).
- drive/capture: cover the Renesas "SAT"-signature capture gate (match + miss).
Craft/docs:
- css/keyless: factor the duplicated LFSR0 output-tap into one helper.
- drive/capture: name the Renesas vendor CDB constants.
- labels/deluxe: use the named ANEWARRAY/NEWARRAY opcodes, not bare hex.
- dvdnav/vmcmd: tighten pub -> pub(crate). dvdnav/mod: mark the parked stub.
- disc/bluray: document that whole-clip extents are intentional (trimming is
PTS-level in SeamPlan). disc/mod + error: doc/comment drift corrected.
- scsi/macos_shim: the DA claim used a stack DAClaimResult passed to an async
callback and never released its semaphore — on the 5 s timeout that is both a
use-after-free (the callback outlives da_hold) and a semaphore leak per drive
open. Heap-allocate it, reference-count it (rc=2), and let whichever side
finishes last release the semaphore and free. (Compiles; needs on-hardware
DA validation.)
- bdnav/index: add a regression test that a title count over MAX_TITLES is
rejected before any record read (untrusted index.bdmv count).
- dvdnav/nav: share ifo::MAX_TT_SRPT_TITLES instead of a second private copy so
the two 99-title caps can never diverge.
Correctness:
- dvd: join the nav main-feature target on the REAL vts_title_num (carried on
DvdTitle) instead of the title's position in the vec, which desyncs when a
sibling PGC is dropped as unparseable or TT_SRPT is non-monotonic.
- diag: the "demoted: no-video" dump now gates on has_probable_video (the gate
main_feature_order actually uses), not the stricter has_video.
Robustness:
- labels/deluxe: clinit_enum_field_names now caps retained bytes
(MAX_CLINIT_LDC_BYTES), like its sibling, so a malicious BD-J .class with a
huge CONSTANT_Utf8 field name can't grow the output unbounded.
Mux (FlagInterlaced correction cleanup):
- factor the PGS/FlagInterlaced deferred byte patches through one patch_bytes
helper; name the InterlacedRewrite record; add ebml::void_element (validated)
for the demotion Void; bound the muxed_* test scans to the pre-Cluster region.
Tests:
- add the FlagInterlaced majority tie-break test (tie stays progressive).
- de-vacuum the profile subtitle-count and meta_sink channel-count assertions.
Docs:
- clpi module doc described an EP-map/sector-range parser that was never
implemented; corrected to what the code actually reads.
FlagInterlaced was written from only the FIRST coded picture, but MPEG-2
progressive_frame is a per-picture flag: a progressive leader on an
interlaced feature (or an interlaced first picture on progressive film)
mislabeled the whole track, so players either deinterlaced progressive
frames or left interlaced frames combed.
Tally every picture's measured scan and correct the reserved FlagInterlaced
byte to the dominant scan at finish() -- the same reserve-then-patch idiom as
PgsForcedFixup and the AC-3/duration fixups. On a demotion to progressive the
now-stale FieldOrder element is Void'd. Also pins the interlaced direction at
the muxed-byte level and adds promote/demote regression tests.
Split the crate into two additive features so a transport-only consumer
(freemkv-flash) can depend on the SCSI passthrough layer without pulling the
ripping dependency tree (serde, zip, rayon, aes, freemkv-unlock, …):
- scsi = the SCSI passthrough transport (src/scsi/ + error): ScsiTransport,
CDB builders, per-OS backends + the macOS cc shim, sense parsing. Pulls only
tracing + the always-on libc target dep.
- rip = the full ripping tree (drive/disc/UDF/MPLS/CLPI/AACS/CSS/mux/keydb +
the freemkv-unlock seam) and all its heavy deps. rip implies scsi.
- default = ["rip", "scsi"], so the full build stays byte-for-byte identical for
the existing consumers; transport-only users set default-features = false,
features = ["scsi"].
All ripping modules are gated #[cfg(feature = "rip")]; error + scsi stay always
compiled. Two read-path timeout consts are rip-gated so a transport-only build
does not warn on unused.
MkvTrack::video sets `interlaced` from the source scan's DECLARED
resolution. An NTSC DVD's IFO declares 480i whether or not its pictures
are coded progressive -- film and animation routinely are -- so
progressive content ships as FlagInterlaced=1, and every player that
honours the flag runs a deinterlacer over progressive frames, softening
every frame of an otherwise bit-exact remux.
The measurement was already there and already trusted for the sibling
property. `apply_coding_to_track` receives the first coded picture's
PictureInfo and derives FieldOrder::Progressive from it
(progressive_sequence / progressive_frame, ISO/IEC 13818-2 6.3.5 /
6.3.10) -- it used that verdict only to clear FieldOrder and then
discarded it. It now also corrects the scan type, exactly as a measured
TFF/BFF already overrides the declaration.
Measured on a real NTSC DVD: ffmpeg's `idet` reports 100% progressive
(multi-frame detection: 0 TFF, 0 BFF, 0 repeated fields) on titles that
shipped FlagInterlaced=1. progressive_frame is set on every picture
coding extension in the stream while progressive_sequence stays 0, which
is the ordinary shape for a film or animation DVD.
Video payload is unaffected -- byte-identical before and after; only the
track header changes.
- Cap every inline `//` comment block at 3 lines; distil the rationale
rather than delete it. Enforced by a new comment-guard CI check.
- Fix stale and orphaned rustdoc: broken intra-doc links (renamed or
removed items), doc blocks that had merged onto the wrong item (leaving
the intended item undocumented), and doc-contradicts-code drift.
- Add a doc-links CI check (cargo doc, deny broken intra-doc links) so
both classes of comment rot are caught going forward.
The macOS SCSI transport took exclusive SCSI access plus a one-shot
`diskutil unmountDisk`, but never claimed the disc from DiskArbitration.
diskarbitrationd was therefore free to remount the volume mid-rip (a
Spotlight re-probe is the usual trigger), yanking the device out from
under an in-progress read; the failure surfaced to the user as E1000.
Hold a DADiskClaim plus a mount-approval dissenter scoped to our disc for
the transport lifetime, so the OS cannot remount it until shim_close().
Link the DiskArbitration framework in build.rs.
Refine the First-Play SPRM-taint abstain to fix two defects:
- Over-abstention: a tainted compare that guards a SetGPRM store (not a
control transfer) no longer forces abstention. The predicate only
decides whether the store runs; a following unconditional dispatch is
still resolvable. Taint now gates only JumpTT/Goto (the control
transfers the resolver follows); a store under a tainted guard taints
its destination GPRM (unknown post-state) instead of abstaining.
- Under-abstention: an SPRM laundered through a GPRM (SetGPRM g = SPRM,
then a later GPRM-only compare on g) previously evaded taint and let
the resolver commit to the cold-machine arm. Add per-GPRM taint bits:
a store propagates its source's taint (SPRM or tainted GPRM) and a
concrete/immediate store clears it; eval marks a compare tainted when
it reads a tainted GPRM.
Adds red-before-green tests for both plus taint-clear guards.
1. dvdnav/vmcmd: the register operand of the if_v1/if_v3 compare layouts is the
LOW byte of the reg-or-immediate field (b[5] / b[7]), not the high byte
(b[4] / b[6]). Fix both layouts and their comment; add register-mode KATs
with distinct high/low bytes.
2. mpls: connection_condition is the low nibble of PlayItem byte[10]
(byte[9] is fully reserved), corroborated by the byte-11 stc_id and
byte-12 in_time reads. Relocate every fixture's encoded value and update the
regression test to assert the reserved byte does not leak.
3. labels/deluxe: escape the two disc-derived class-name tracing fields via the
Debug path, matching the other log sites.
4. disc/dvd: add an integration test that drives scan_dvd_titles with a resolved
First-Play nav target so the 1-based promotion join is verified end-to-end.
5. bdnav/vm: add unit tests for the SET/SWAP immediate gate — full register
exchange, and that an immediate-flagged operand is never a write target.
6. dvdnav/nav: honor the documented contract — a First-Play compare that reads a
system parameter is undecidable at scan time, so eval now taints on SPRM reads
and the executor abstains rather than committing to the cold-power-on arm.
GPRM reads never taint.
7. labels/deluxe: narrow four now-internal helpers from pub(crate) to private.
Add an additive coverage job to CI that generates lcov via cargo llvm-cov
and uploads to Codecov (tokenless). Mirrors the test job's pinned 1.94
toolchain and dual freemkv-unlock checkout for the path dep. Informational
only: no gate depends on it. Adds codecov.yml and a README badge.
Audit round-2 (fix-adversary): the round-1 deluxe sweep fed the zip local-file-
header magic, but ZipArchive::new validates the end-of-central-directory at the
buffer's end — which the byte generators (front magic + noise) can never
synthesize — so the archive open always failed and decode_binding was never
reached: false coverage for the very bytecode-decoder panic path it guarded.
Drive decode_binding_class directly on a parsed .class with an empty master-enum
table instead, so the symbolic stack machine is actually exercised.
Continuing the /codeaudit HEAD..v1.6.10 round:
- disc/mod.rs rank_titles: bound the O(n^2 * clips) composite scan to a
sub-second work budget (both title count and per-title clip count are
untrusted disc metadata); above it, skip composite classification and degrade
to the has-video + canonical order. Reorder the per-pair checks so the O(len)
is_subset runs only after the cheap video/size gates. (regression test)
- bdnav/vm.rs SWAP: honour the imm_op1/imm_op2 store gates so a SWAP to an
immediate operand does not corrupt a register.
- harness.rs: fuzz the previously-uncovered new parsers — the Deluxe bytecode
decoder, the Fox dcx.xml scanner, and the combined bdnav index+mobj+VM resolve
path (labels::{deluxe,fox} and bdnav::vm made pub(crate) for in-crate fuzzing).
- dvdnav: pub(crate) the module + submodules (was pub) to match bdnav and stop
leaking the DVD IFO/VM internals as public API; drop an unused re-export.
- labels: escape disc-derived strings in diagnostics (Debug not Display) so a
hostile disc cannot inject terminal control sequences into logs.
From the /codeaudit HEAD..v1.6.10 round (each verified against the libbluray
HDMV VM reference, and design-refuted SOUND on two models):
- bdnav/vm.rs title_obj: resolve First Play via the spec sentinel 0xFFFF (was
num_titles+1). A JumpTitle 0xFFFF now reaches First Play, and an out-of-range
JumpTitle(N+1) correctly abstains instead of mis-jumping there.
- bdnav/vm.rs wr: refuse stores to PSR-tagged registers (matches _store_reg),
so an illegal SET/SWAP to a PSR is a no-op and later compares agree with a
real player instead of diverging.
- disc/profile.rs main_title(): return Option<&TitleProfile> instead of indexing
titles[0], which panicked on a disc that scanned to zero titles.
Regression tests added, each shown red-before-green.
The MSRV split raised the dependents above libfreemkv's own floor — the
bins (freemkv, autorip, bdemu) are 1.98 and freemkv-keysources is 1.97 —
so the consumers job could no longer build them at libfreemkv's 1.94
toolchain (`rustc 1.94.0 is not supported by ... freemkv-keysources
requires rustc 1.97`), leaving dev red since the MSRV change. Pin that one
job to 1.98, which satisfies every dependent's floor. libfreemkv's own
lib/lint/test jobs stay at its 1.94 MSRV.
A nav- or authoring-resolved playlist could promote a video-bearing but
negligible-payload branch above the substantial feature it sits beside:
Spotlight (2015) UHD navigates to a Dolby-Vision seamless-branch playlist
(00001, 0.4 GB across 102 tiny clips) instead of the 91.5 GB single-clip
feature (00002). A player plays the branch; a rip wants the feature.
Require a nav/authoring-promoted title to also carry at least 10% of the
largest video-bearing title's bytes; otherwise selection falls through to
the composite/size ranking, which recovers the real feature. Adds the
Spotlight-shaped regression test.
Verified against the 88-disc hoard: title selection is identical to the
prior release on every disc except the one it fixes (Spider-Man 3 UHD,
00245 wrapper decoy -> 00001 feature).
Also clears three 1.97 clippy lints in the HDMV VM and drops two
reference-implementation filename citations from the bdnav module docs.
Implement the DVD navigation feature resolver (#40): read the disc's own
First-Play program out of VMGI (VIDEO_TS.IFO) and execute it on a bounded
register machine to identify which title a player would land on, then prefer
that title in DVD main-feature selection.
- src/dvdnav/nav.rs: new module.
* Parses the VMGI First-Play PGC command table (FP_PGC pointer at VMGI+0x84,
PGC command-table pointer at PGC+0xE4, pre-command list) and TT_SRPT
(title -> VTS/VTS_TTN, pointer at VMGI+0xC4).
* Executes the pre-command list with the existing vmcmd decoder over 16 GPRMs
+ SPRMs from a cold machine: SetGPRM (mov/add/sub/mul/div/mod/and/or/xor),
compare predicates, Goto, Break/Exit, and JumpTT (title dispatch). Menu /
sub-domain jumps and intra-domain links are treated as undecidable.
* resolve_from_vmg(&[u8]) is pure and total; resolve_main_title(reader, udf)
reads the IFO and returns the resolved (title, vtsn, vts_ttn) or None.
* Every offset/count is bounds-checked; a step budget bounds non-convergence.
- src/disc/dvd.rs: scan_dvd_titles now resolves the nav main title and returns
its playlist_id alongside the titles; test call sites updated for the tuple.
- src/disc/mod.rs: the DVD nav pick feeds sort_titles_by_main_feature as
nav_feature, the same seam the BD path uses, guarded to a video-bearing
title. None -> today's leading-cell heuristic stands (strict improvement).
- src/harness.rs: add resolve_from_vmg to the never-panic sweep.
Tests: 11 nav unit tests (unconditional/conditional/set-then-dispatch convergence,
menu/empty/out-of-range/self-loop/truncation/overflow abstention) built to the
IFO spec using the real First-Play command bytes from the existing decoder KATs
(unconditional JumpTT 1; menu entry). Full suite green on 1.94.
Add a thin, format-agnostic typed VIEW over the scanned Disc model so
downstream reads `profile.titles[i].subtitles[j].forced` (and analogous
audio/video flags) for any disc with no per-format conditionals.
New src/disc/profile.rs:
- DiscProfile / TitleProfile / VideoTrack / AudioTrack / SubtitleTrack
(serde Serialize + Deserialize), re-exported from the disc module.
- DiscProfile::from_disc / From<&Disc>, Disc::profile(), and accessors
(DiscProfile::main_title, TitleProfile::video/audio/subtitles).
- Splits each title's Stream enum into three typed vectors; flattens
normalized fields (language default "und", forced/sdh from the subtitle
qualifier, commentary/descriptive from the audio purpose, label -> name).
- Hoists derived flags so downstream never recomputes: main_title (the
pre-sorted main feature at index 0), is_main, and the default
audio/video track (first non-secondary, keep only the first), subtitle
default always false.
Make the profile types the single serialization source of truth: the
json:// sink now serializes TitleProfile instead of hand-building JSON,
so json:// and any downstream share one schema. Stream arrays are split
by kind and qualifier/purpose decomposed into booleans; json://-only
extras (clips, full chapter_marks list) are spliced back on. meta_sink
tests reconciled to the new schema.
Tests: profile normalization across BdTs + MpegPs titles, typed-vector
split, populated fields, hoisted main/is_main/default flags,
qualifier/purpose mapping, and a serde round-trip.