shim_list_drives: reorder the drain loop condition so IOIteratorNext is
not called on the exit iteration, releasing every obtained io_service_t.
shim_open_exclusive: the fallback matcher ignored the requested bsd_name
and could open a different drive; iterate and accept only the candidate
whose IOMedia BSD name matches, else fail. Correct the misleading da_hold
TOCTOU comment.
class_reader: checked pc + size add (guards 32-bit overflow) and remove
the dead BadInstruction variant.
criterion: dedup duplicate stream numbers instead of emitting a number
twice. deluxe: continue past a bad studio attribute instead of aborting
the whole parse. bdmt: treat mixed-content XML (embedded markup) as XML
so it is dropped, without over-rejecting a bare '<' in prose.
pes: grow the frame buffer incrementally instead of an eager
MAX_FRAME_SIZE allocation. extract: give the case-probe a unique random
suffix so concurrent probes cannot collide (TOCTOU-safe).
network: add the NAT64 (64:ff9b::/96) arm to the IP classifier -
extract the embedded IPv4 from the last 32 bits and re-check it, matching
the keysources canonical copy, so a NAT64-wrapped private/special
address is blocked like the bare IPv4.
error: add Display arms for DirInsufficientSpace (E9027), DirNameCollision
(E9028), DirWriteFailed (E9029) so their fields render (codes-only, like
the sibling Dir variants).
hex: reject a leading + in the hex int parser.
inf: read_mkb_from_drive used .is_ok() after the first pack, silently
truncating the MKB on a mid-walk transport error; propagate the error
instead. Add checked offset guards (94/117) and reject a truncated title
table (161) like the key list already does.
segment: the FMTS key-range carry assumed 2048-aligned segment starts;
use the floor-difference of the actual LBAs so a non-2048-aligned
segment carries correctly.
Annotate css::keyless::recover_title_key as a test-only helper and
correct the clpi_audit dedup-by-PID doc.
lpcm: carry last PTS + DTS fallback (add last_pts_ns) and propagate
pes.discontinuity instead of hardcoding false, matching adts/flac/
mpegaudio.
pesbuf: add an internal MAX_BUFFERED_BYTES cap (defense-in-depth like
reorder.rs's MAX_GOP) so a misbehaving caller can't grow it unbounded.
dts: drop a truncated trailing core at end-of-stream instead of emitting
it, while keeping genuine trailing extensions.
hevc: resolve num_extra_slice_header_bits from the PPS the slice
references (slice_pic_parameter_set_id), not the last-active PPS.
crc: remove the dead, misnamed crc8_atm (impl was CRC-8/SMBUS, no
callers). Add the missing drop-gate mismatch test; fix the startcode
read_bits contract doc and the h264 SEI comment.
mkvstream: add the consumed > remaining => MkvSourceInvalid guard (as
BLOCK_GROUP already had) to the INFO/TRACKS/TrackEntry/Video/Audio child
loops, and route the DURATION/SAMPLING_FREQUENCY float reads through the
checked_size cap instead of truncating cs as usize.
mp4/read: audio sample_rate was taken from the mdhd timescale, ignoring
the AudioSampleEntry 16.16 samplerate at the stsd body; read the real
field and fall back to timescale only when it is 0. Drop the unreachable
MAX_TRACKS break (the box scan is already capped) for a debug_assert.
tsmux + m2ts_mux: PTS origin was seeded on video only, so audio
preceding the first video (or an audio-only stream) rebased onto its own
PTS and lost A/V offset; seed the origin from the first frame of any
kind.
demux_sink: dropped_total() folded drops across ALL tracks while
frames_mapped now counts only persisted frames, so a filtered
(sub://audio://) multi-clip export could false-trip SeamPlanDroppedMost /
SinkWroteNothing. Compare drops and mapped frames over the same
persisted-track set via TimelineContinuity::dropped_for; update the
frames_mapped doc to the persisted-only semantics.
resolve: resolve_mux_key_map(...).ok() swallowed the error silently
(TrueHD channel correction skipped with no trace); log it like the
sibling reopen() fix before skipping.
meta_sink: TitleProfile::from_title dropped json:// stream fields
(pid, color/aspect/dimensions/interlaced/mvc, audio secondary/
sample_rate/purpose, subtitle pid/descriptive_service); restore them at
the json document level.
disc/encrypt: on the disc-keyed cert route, drive_unlocked was derived
from is_drive_unlocker(matched), which is provably always false -
run_bus only ever dispatches the disc unlockers (AACS/DVD), never a
firmware/drive unlocker. Hardcode false with an honest comment; the
genuine drive-unlock case (drive_unlocked, no read_data_key) is the
OEM-VID short-circuit and the bus-key gate already credits it. Gate the
now test-only is_drive_unlocker behind cfg(test) and add a regression
test through resolve_vid_only.
aacs/derive: resolve_dk_node looped 0..u_mask_shift shifting 1u32 << b,
which panics/wraps when the disc/keydb-controlled u_mask_shift is >= 32.
Cap the search at the 32 real bit positions.
drive: unlock_tray dropped its SCSI result with let _; warn! on failure
so a stuck medium-removal PREVENT lock is visible. Fix the stale
probe_disc doc (now a no-op; calibration moved into the unlocker).
DecryptingSectorSource::with_content_ranges was a silent no-op: the
proactive AACS key_map path early-returned before consulting the content
extents, and decrypt_span ignored its _content argument. A whole-disc
reader (sweep/patch) that installed both a key map and content ranges
therefore decrypted/verified clear UDF/BDMV-nav units, and its
misaligned filesystem reads tripped the AACS unit-alignment gate.
Thread the content extents through decrypt_sectors_mapped_in_content ->
decrypt_span -> apply_aacs_map, skipping any unit whose LBA falls outside
every content range (never decrypted, verified, or counted as loss), and
gate the alignment check on span_touches_content so out-of-content
filesystem reads pass through untouched.
Also drop the redundant is_multiple_of clause in the prefetched
unit-tail guard (remaining > 0 is already guaranteed).
Reverts the cross-repo E7031 addition: it forced enumerating the code across
freemkv test_support + freemkv-i18n's 29 locales for a LOW config-fault nicety.
keysources config faults keep borrowing the transient key-service code until a
coordinated i18n release; decrypt_bus-pub and the SSRF-parity additions stay.
- aacs/content: widen decrypt_bus to pub so freemkv-drivetest can invert
AACS 2.0 bus encryption out-of-tree (was pub(crate), broke that build).
- mux/network: port the 6to4 (2002::/16) and Teredo (2001:0000::/32)
tunnel-decode arms and the 198.18.0.0/15 + 192.0.0.0/24 blocks from the
canonical keysources SSRF classifier so both cover the same set.
- error: add distinct KeyServiceConfig variant (E_KEY_SERVICE_CONFIG 7031)
for the standing key-service misconfiguration fault, separate from the
transient KeyServiceUnavailable; keysources' online.rs adopts it later.
- dirimage: add a MAX_DEPTH nesting-cap test that walks a real
deeper-than-cap directory chain and expects DirImageTooLarge.
- udf: add a long_ad (ICB flags = 1) directory read test — the LBA sits
at AD offset 4, not 12 like extended_ad.
- labels/paramount: derive the feature hint's id and filename from one
parsed number (canonical 5-digit NNNNN.mpls) so they can't disagree,
the same fix already applied to fox, and add a hint-parse test.
Add unit tests for to_c_path (single trailing NUL, path bytes preserved
for libc::open) and resolve_to_sg (sg passthrough plus the no-filename
and non-sr/sg fallbacks that need no sysfs) — the helpers the open /
drive_has_disc / recovery reopen path relies on. Verified compiling for
x86_64-unknown-linux-gnu (this module is Linux-only).
- drive: SET CD SPEED and PREVENT/ALLOW MEDIUM REMOVAL are best-effort; a
drive rejection must warn, not fail the rip. New test drives a failing
transport through set_speed/lock_tray/unlock_tray.
- bdnav: CallObject/CallTitle follow the same play path as JumpObject/
JumpTitle for feature resolution; add tests for both.
- mkv: FlagInterlaced is corrected at finish() to the whole-stream scan
majority; add a test that a progressive leader on an interlaced feature
is patched from progressive to interlaced.
Add a direct Vm::set test exercising mov/add/sub/mul/div/mod/and/or/xor
and the divide- and modulo-by-zero guards (register left intact, no
panic) plus an unmodelled-op no-op, and a First-Play test that a
SetSystem instruction keeps resolution running so a following JumpTT
still resolves.
- bdnav/mobj: drop the unreachable MAX_CMDS cap (num_cmds is a u16 and the
span is already bounded against the input) and its flagged NOTE.
- drive/wait_ready: break out immediately on a dead bus (transport
failure) instead of polling a phantom for the full ~30 s; add a test.
- css/is_scrambled_pack: document that the 0x11 stream_id offset assumes
zero pack stuffing (true for DVD-Video VOB packs) and why a stuffed pack
degrades safely.
- scsi/macos_shim: document why releasing g_handle_lock before da_hold is
TOCTOU-safe (da_hold re-validates the handle under the lock).
passive_offset tagged a provisional offset with epoch_offsets.len() as
the epoch identity, but that length is capped at MAX_EPOCHS. Past the cap
the ring's remove+push leaves len() constant, so a provisional captured
then compared equal to the current identity forever and never retired,
drifting that track off every other one for the rest of the title. Add a
monotonic epoch_seq bumped once per open_epoch and tag provisionals with
it. Add a past-the-cap retirement test.
mpls: the STN table gate required item.len() > STN_OFFSET + 16, one byte
more than the 16-byte header actually spans, skipping a first play item
whose STN header ends exactly at the item boundary. Use >=, with a
boundary test.
ifo: the subtitle palette layout [padding, Y, Cr, Cb] (Cr BEFORE Cb) was
correct but under-documented; harden the comment against a future
byte-swap and add a test that cross-checks the parse against the DvdSub
ycbcr_to_rgb consumer so the two read Cr/Cb the same way.
fox: playlist_id and filename were derived independently from the raw id,
so a 5-digit id above u16::MAX left playlist_id None while the filename
stayed Some — a half-hint naming different things. Derive both from one
parsed number via a feature_hint() helper with the canonical NNNNN.mpls
filename, and test that the fields agree.
deluxe: parse_studio_attr anchored on a bare find("studio"), which also
matched substudio=/studioId= and the word 'studio' inside another
attribute's value, then read a later attribute's quotes as the studio.
Require a word boundary before it and '=' after, and add anchoring tests.
read_sectors fabricated zeros and reported success for any LBA, including
one at or beyond the image's own capacity — an oversized/crafted offset
that a real file/ISO source rejects. Refuse a read whose START is past
capacity (as read_exact does past EOF) while still zero-padding a batch
that starts in range and overshoots the end, which the mux's final batch
and the FS scan's fixed-LBA probes rely on. Split the read-range test to
pin both behaviours.
The TrueHD channel-correction probe's reopen closure returned Option<S>
via .ok(), collapsing a real io::Error into an indistinguishable None.
Make reopen return io::Result<S> and log the actual error on the failure
arm so a failed re-open is diagnosable, not silent.
unlocker_names() was a hand-maintained list that could drift from the
unlockers that actually dispatch, and the cert route hardcoded
drive_unlocked:false. Factor the firmware and disc unlocker sets into
single constructors, derive unlocker_names() and a new is_drive_unlocker()
from their real .name(), and set HandshakeResult.drive_unlocked from
whichever unlocker claimed the drive so the bus-key gate credits a drive
unlock even on the cert dispatch path. Add derivation and classification
tests.
The extract collision check always folded names to lowercase, so two
disc files differing only by case (Movie/movie) aborted the extract even
on a case-sensitive volume where they are distinct, coexisting files.
Probe the real target directory's case-sensitivity and fold only when the
host would (APFS/NTFS), defaulting to case-insensitive when the probe
can't run. Update the case-collision test to track the real volume, and
drop a dead duplicate pack-start write in the CSS cross-contamination test.
When a PTS-less PES arrived with pending_pts invalidated (e.g. after a
forced flush), the fallback fell straight to 0, snapping the timeline
backwards. Add continuation_base_ns() to reach for the projected next-AU
start, then the last PES front, before conceding 0, and stop stashing a
drained buffer's stale front mark as pending_pts. Add a fallback test.
read_directory read only the FIRST allocation descriptor of an out-of-
line directory, so a FID list spanning multiple extents was truncated at
the first boundary, and a type-3 continuation descriptor was mis-read as
FID data. Walk the whole descriptor list (types 0/1/2 as allocated FID
extents with the 2-bit type masked out of the length; type 3 followed as
a continuation block, bounded by MAX_AD_BLOCKS), gathering every extent
into one buffer while tracking the declared FID-list length separately
from the sector-rounded buffer. Bound the FID walk and the name read by
that declared length, and read a FID whose 38-byte header ends exactly at
the boundary (<=, not <) while still rejecting one whose name spills past
it. Add a multi-extent directory test.
The open-GOP keyframe promotion trusted only the first slice's coding
type and wasn't gated on the base view. A picture whose first slice is I
but whose later slices are P/B is not a clean random-access point, and an
intra MVC dependent view is never an independent anchor. Track every VCL
slice's coding type (all_vcl_intra) and require a real VCL slice, then
promote only when all slices are intra AND !mvc. Add mixed-slice and MVC
non-promotion tests.
The MAX_META_BYTES guard multiplied (dir_count + file_count) by the
sector size, counting one File Entry per node but omitting each
directory's FID-list data sectors. A wide, shallow tree's directory data
can dwarf the per-node entries, so an oversized folder could slip past
the guard and be materialized in full. Gate on metadata_block_count()
instead, which already sums File Entries and FID-list sectors, and add a
test proving the FID-list sectors are counted.
frames_mapped incremented for every mapped frame, including kind-filtered
ones that write to no track file (e.g. video during an audio:// export).
That inflated the denominator so a seam plan dropping every persisted
frame could ship zero-byte files at exit 0. Move the increment inside the
write block so it counts persisted frames only, and add a test where more
frames are seam-dropped than persisted -> SeamPlanDroppedMost fires.
ImageTruncated fell through to the bare-code catch-all, dropping the
have/want byte lengths a truncated-resume report needs. Add an explicit
arm emitting them in the identifier-free E{code} have/want form.
read_first propagated every non-UdfNotFound error immediately, so a
DiscRead on the primary Unit_Key_RO.inf aborted instead of trying the
/AACS/DUPLICATE/ backup that AACS ships for exactly this case. Treat a
DiscRead as retriable like an absent candidate: continue to the next
path, remembering the error, and only propagate it once every candidate
has been tried (still more informative than a bare AacsNoKeys). Add
fall-through and propagate-after-exhaustion tests.
find_boxes_capped used plain `pos + box_size` on an attacker-controlled
64-bit largesize cast to usize. A value near usize::MAX at a nonzero pos
wraps past the length guard, producing a release panic on the payload
slice or an infinite loop on the advance. Mirror read_moov: compute the
end once with checked_add, reject on overflow or overrun, and reuse it
for the guard, slice, and advance. Add the hostile-largesize test.
scsi::windows::list_drives delegated to crate::drive::windows::find_drives(),
but the drive and identity modules are #[cfg(feature = "rip")]. A scsi-only
build (default-features = false, features = ["scsi"]) — which is exactly how
freemkv-firmware consumes libfreemkv — therefore failed to compile: 'could not
find drive in the crate root'. The --all-features CI never saw it (rip is on).
Enumerate directly at the SCSI layer via scsi::open + scsi::inquiry, mirroring
the Linux and macOS backends, which already build their DriveInfo without the
rip-gated modules. The rip-side drive::windows::find_drives() is untouched.
Verified: cargo check --target x86_64-pc-windows-msvc passes both scsi-only and
with default features.
A drive claimed by a firmware unlocker (freemkv/LD/Renesas) at init() is that
unlocker's sole VID source. When it stashed no VID, do_handshake_cert now returns
(None, None) instead of running the AACS cert route (run_bus's AacsUnlocker) whose
REPORT KEY AGID cycle poisons the drive's AGID selector so every later bare 0xAD
VID read aborts until the medium is reloaded. A key source can still supply the key.
Adds a red-before-green test proving the guard issues zero SCSI on that path.
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).
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.
- 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.
The Deluxe parser detected com/bydeluxe discs but emitted nothing, so
mpls_universal won at Low. Grounding the decoder on real Universal discs
(Fast Five / F9 / Bourne, studio="uni") surfaced three defects that
kept the full Phase A-D pipeline from ever resolving a stream on any
real disc, all fixed here:
1. Master enums were keyed by the ZIP ENTRY name ("pd.class") but the
binding class references enum constants by JVM INTERNAL name
(getstatic pd.<field>), so Phase C's getstatic count never matched
the identified enums and found zero binding classes. Key master enums
by this_class_name instead.
2. MasterEnumTable resolved getstatic operands by enum VALUE string
("English") but the operand is the obfuscated static-FIELD name
("a"), so every language/purpose ordinal came back unresolved.
Capture the ordered putstatic field names (clinit_enum_field_names)
and key the resolver on them.
3. The Language-enum fingerprint demanded 70+-4 values; Universal's
enum has 65, so Phase A rejected it outright. Give each fingerprint
its own count window (Language widened to span 65 and 70); the
ordered 4+-string prefix stays the real discriminator.
Also: model anewarray/newarray so array construction keeps the symbolic
stack aligned, and skip constructors with an array parameter so the
Universal title-wrapper object (oq.<init>(...,[Lnp;,[Lwb;,...)) is not
mistaken for a subtitle binding. Recover SDH/RNIB from the language
display name (Universal encodes it as a distinct Language value, not a
Purpose). Add config.xml <TitleConfig studio="..."> detection. Raise
confidence to High when the binding table decodes >=1 stream; the
mpls_universal floor still back-fills un-named STN slots.
Fixtures: verbatim pd/lp/tl .class files from FastFive.iso decode to
English/French/Spanish + English Commentary (Dolby Digital) audio and
per-title eng/spa/fra subtitles. Validated end to end: labels-analyze on
FastFive/F9/Bourne now reports parser=deluxe High with real per-stream
languages, names, purposes and codecs instead of mpls_universal Low.
Treat Java bytecode as a documented format we READ (never execute), a
peer of mpls/clpi. The class reader's bounds checking was already sound
(typed Reader with checked u8/u16/u32/u64 + slice, saturating switch
arithmetic, iterator halt-on-truncation); this adds the untrusted-input
entry points the never-panic harness was missing and the malformed-input
regression tests the format warranted.
- harness: add class_reader (magic CAFEBABE) and jar (zip PK header)
sweep targets. The class_reader target also drives the accessors a
label parser exercises (this_class_name, member Code walk, cp_index,
member_ref, load_constant_display) so their indexing is fuzzed, not
just the structural parse. The jar target opens the sweep bytes as a
zip and walks every .class through for_each_class exactly as deluxe/dbp
do.
- class_reader tests: oversized constant_pool_count and interfaces_count
reject with UnexpectedEof (u16 counts keep with_capacity bounded);
out-of-range cp indices return None from every accessor; a Code
attribute whose declared code_length runs past its body is rejected.
scan_with runs the bdnav VM for BDMV discs and feeds the video-gated result
as the new top MAIN_FEATURE_ORDER key (nav-feature > authoring-feature >
standalone > has-video). A nav result is only honoured for a video-bearing
title, so a mis-resolve can never reopen the decoy hole; when the VM abstains
(BD-J, non-convergence) the structural/heuristic order decides exactly as
before. Adds nav selection tests and updates the diag decision reason.
Add src/bdnav: a libbluray-faithful, bounded, never-panic HDMV navigation VM
that runs First-Play with correct power-on PSR/GPR state to resolve the
playlist the disc itself plays as the feature. One generic VM handles the
obfuscated Sony dispatcher and every other HDMV disc with no vendor
special-casing; it abstains (None) on the BD-J boundary, malformed data, or
non-convergence. index/mobj parsers added to the never-panic harness.
Older Fox discs ship a loose plain-XML manifest at
/BDMV/JAR/<id>/dcx.xml (root <dcx>) that names every feature stream's
language, editorial purpose and forced/SDH state in attributes — fully
parseable and previously unowned (2 Fox discs matched no parser, 11 were
mis-owned by the deluxe stub).
New src/labels/fox.rs:
- detect(): loose dcx.xml under /BDMV/JAR/*/ (cheap dir walk), falling
back to a com/foxbd/ jar central-directory prefix so newer Fox discs
are attributed to Fox even though their bytecode form isn't decoded
yet (Phase 2, sketched in-file).
- parse(): scopes extraction to one <playlist name="feature"> element
(the richest, to avoid merging regional 00800/00801 STN tables),
reusing labels/xml.rs. audio type=rnib -> Descriptive, comment* ->
Commentary; subtitle form=sdh -> Sdh, type=embed -> Forced; nested
id="NN" -> 1-based per-type STN slot. High confidence.
- Feature playlist id recorded via tracing::debug for now; dev's
ParseResult has no feature_playlist field (that FeaturePlaylistHint
slot lives on selection-hardening). An in-code note documents wiring
it at reconciliation, matching paramount::parse.
Registered ahead of mpls_universal, alongside the High-confidence vendor
parsers; registry-order lock test updated.
Tests embed the real Life of Pi dcx.xml (/BDMV/JAR/05001), asserting the
00800 feature selection + id, all 11 audio labels (incl. the eng rnib
descriptive track) and 11 subtitle labels (sdh/forced/none), the
no-cross-playlist-merge scope rule, plus negative-detect cases.
The has-video gate alone fixed Spider-Man 3 UHD only by accident: the
00245 decoy = [bumper][00001 feature clip][outro], and streams come from
PlayItem 0 (the bumper) only, so it reported v=0. That is fragile — a
video-bearing composite [feature][outro] still beat the feature on size
(audit R2), and a real feature with a non-video first PlayItem would be
wrongly disqualified (R6).
Add structural clip-set composite detection: a title whose clip set
properly contains another substantial video title's whole clip set is a
play-all/wrapper composite and is demoted below the standalone title it
wraps — independent of stream counts, physical size, and disc capacity
(which is 0/unknown when READ CAPACITY fails, disabling the oversize
gate). Replace the has-video disqualification with the more-permissive
has_probable_video (real video, or size+duration+clips consistent with
video) so a bumper-lead feature is never disqualified while streamless
menus still are. Corroborate the authoring hint by duration (a hint at a
short branch/trailer can't override the feature) and require real video
streams for it. Precedence: authoring -> standalone-over-composite ->
has-video -> canonical (with a reserved top slot for the future BD-nav
key). Unify the session.rs key-sampling/verdict pickers and
image_crack_extents on has_probable_video.
Tests: video-bearing composite (T2), oversize composite with unknown
capacity (T3), short-branch hint not honoured (T4), bumper-lead feature
not disqualified (T6), all-streamless deterministic + single-source pick
(T7), equal-clip twins not composite, video-only title. T2/T3/T6 fail
under the prior has-video-only logic.
Main-feature selection ranked purely on physical size -> duration ->
audio (canonical_title_order), with no requirement that the chosen title
carry a decodable video stream. A playlist-obfuscation decoy can inflate
a streamless playlist past the real feature on size AND duration (Spider-
Man 3 UHD: 00245.mpls, 78.4GB/8350s/v=0 beat 00001.mpls, 76.5GB/8038s/
v=1), so the mux ran a title with zero streams -> zero frames -> E6008.
- Add Disc::main_feature_order layering two gates atop canonical_title_order:
authoring-feature (a title the disc's menu authoring designates AND that
has video) wins outright; then has-video demotes any streamless title
below every title that has video. scan_with re-sorts with it after labels
run so titles[0] is the selected feature. New MAIN_FEATURE_ORDER_KEYS +
DiscTitle::has_video().
- Surface the authoring feature-playlist id: labels::FeaturePlaylistHint,
captured from Paramount playlists.xml <playlist name="Feature" id=..>,
threaded through ParseResult/extract; labels::apply now returns it.
- Unify pickers so key-sampling and the structural verdict (session.rs) and
image_crack_extents prefer the has-video feature, not a streamless decoy.
- diag: decision row logs video= count + main_feature_order reason and a
tag=decision.demoted line per no-video title (closes the field-diagnosis
gap this incident exposed).
Tests: main_feature_order_demotes_streamless_decoy (pre-gate vs post-gate
contrast), _honours_authoring_hint, authoring_hint_never_promotes_a_
streamless_title, feature_playlist_hint_matches_on_id_or_filename; diag
reason test updated. QA green on 1.94 (fmt/clippy -D warnings/test).
Lower the crate MSRV from 1.97 to 1.94 -- the lowest toolchain on which
`cargo build`, `cargo clippy --all-targets -D warnings`, and
`cargo test --tests` all pass clean. Dependencies floor the build at 1.90
(freemkv-unlock 1.90, zip 1.88), but clippy is only warning-clean from
1.94 (older clippy has a `duplicated_attributes` false positive on
`#![cfg(test)]` and flags a now-documented `let_and_return`). Update
`rust-version` and every `dtolnay/rust-toolchain` CI pin to 1.94.0, and
add a CHANGELOG Unreleased entry.
Extend the byte-provenance coverage added in f7d38ca (currently one
TS-path test) with two PS-path tests driving the real `Mpeg2Parser`:
- `ps_stream_stamps_source_provenance`: the PS path forwards `ps.source`
onto `PesPacket`, so demuxed frames carry `Some(SourcePos)`.
- `ps_stream_carries_feed_base_across_reads`: the cumulative `fed_bytes`
base carries ACROSS read buffers -- a frame decoded from the second
sector is stamped `source.byte >= 2048`, not reset per read.
Also record the f7d38ca provenance fix in the CHANGELOG, inline a
`let_and_return` test binding, and trim a redundant sentence from
`PsDemuxer::feed_at`'s doc comment.