Fix (B) — the reproducible bug behind ffmpeg "Timestamps are unset in a
packet for stream N" on PGS subtitle tracks (worst on the sparse forced
track). Subtitle tracks carry no DefaultDuration, so a frame emitted with
duration_ns: None became a bare SimpleBlock with nothing to bound the cue.
- codec/pgs.rs: synthesize a fallback dwell (DEFAULT_PGS_DURATION_NS, 5 s)
for every emission that was duration_ns: None — the EOS trailing set and
the no-PTS clear/display, truncated, and lone-pass-through arms. emit_pending
keeps its real computed span but clamps a pathologically large one
(missing intermediate PCS) past MAX_PGS_DURATION_NS back to the fallback.
- mkv.rs: track_is_subtitle (track_type == 17), mirroring track_is_video. In
write_frame_at a subtitle frame with no duration now substitutes a minimum
fallback (MIN_SUBTITLE_BLOCK_DURATION_NS, 1 s) so it is emitted as a
BlockGroup WITH BlockDuration, never a bare SimpleBlock (defense in depth).
Fix (A) — defensive HEVC guard (the "Invalid NAL unit size" error is not
reproducible in our clean disc->MKV path; this makes our output structurally
incapable of originating it):
- codec/hevc.rs: length_prefix_tiles() self-consistency check — the
[u32-BE len][body] records must EXACTLY tile the frame; on mismatch the
desynced access unit is dropped (logging src position) rather than emitted.
- LENGTH_PREFIX_SIZE constant + regression test that hvcC byte-21
lengthSizeMinusOne+1 equals the prefix width push_length_prefixed writes.
Tests: pgs.rs None-arm duration assertions updated to the fallback + a
clamp/no-clamp pair; mkv.rs subtitle-BlockGroup and two-track (dense+sparse)
"every block has a BlockDuration" cases; hevc.rs tiling accept/reject,
byte-21 regression, and a positive parsed-AU-tiles-exactly test.
A BD-J disc (Alita) has no authoritative static playlist chain, so main-
feature selection is a structural failsafe. Its chaptered feature 00800 is
a SUPERSET of an un-chaptered body subset 00703 that is 0.97 of its size,
so the size-only seamless bumper limb fired and demoted the real feature,
ripping the body instead.
Gate ONLY the bumper limb on is_complete_feature_presentation(q): the
wrapped/subset title must carry a real chapter table (>= MIN_FEATURE_CHAPTERS
marks spanning >= CHAPTER_SPAN_MIN_FRAC of its runtime). The concat limb
(much-shorter cut) is untouched. This is the ABSOLUTE-threshold predicate;
a one-line swap to the RELATIVE form (p.chapters.len() <= q.chapters.len())
is documented at the call site and in the predicate doc, pending a hoard
sweep of SM3's real chapter counts.
Also add lowest playlist_id as the final canonical_title_order tie-break so
equal-size seamless siblings (00800-00808) pick 00800 deterministically.
Fixtures re-based on the real chapter discriminator (not a size coincidence)
and new tests cover both topologies (Alita superset-feature, SM3 subset-
feature) plus the un-chaptered-subset keep-superset case.
The keydb lookup returned an empty Vec for two very different outcomes:
a true hash miss, and a MATCHED disc that derived no key (e.g. an MK-only
entry with no VID). Both rendered as `keydb > no entry`, misleading
reporters whose disc IS present in the keydb.
Add KeySource::resolve_unit_keys -> UnitKeyResolution { keys, matched,
miss_path, matched_entry, store_entries }; the trait default delegates to
get_unit_keys (matched=false) so every existing source is unchanged.
resolve_and_apply_traced now renders MatchedDisc + NoVid/NoDerivableKey
for a matched-but-no-key disc and reserves NoEntry for a genuine miss.
New KeyNode::NoDerivableKey, a MatchedEntry shape (booleans + lengths, no
key material), and KeyStep.{matched_entry, store_entries} carry the
diagnostics to the app-layer renderer. Tests cover both verdict paths.
Bus content-range de-bus:
- F1 disc/mod.rs: always install content ranges (extracted wire_bus_removal);
empty ranges => Some([]) => de-bus NOTHING (fail-safe), not None => de-bus
everything — closes silent clear-sector corruption on the AACS cert route.
- F4 aacs/content.rs, F5 decrypt.rs: content gate is now a partition_point
binary search (O(log n)/sector) over the sorted, merged-disjoint ranges,
replacing the per-sector O(ranges) linear scan on the de-bus hot path.
- F2 scsi/linux.rs: RECOVERY_THREADS cap uses atomic reserve-then-check
(fetch_add first, fall back on overshoot) — removes the TOCTOU that let
concurrent multi-drive rips exceed MAX_RECOVERY_THREADS.
- F3 pes.rs: PesFrame::deserialize reads each chunk straight into data's tail;
drops the scratch Vec + double-copy, keeps the incremental-growth DoS guard.
- F7 disc/mod.rs: six inline rank_titles thresholds -> named consts.
Tests: bus fail-safe + scan wiring (both routes), per-sector gate straddle,
read_directory type-3 continuation/AED-skip/cycle-guard, strengthened
without_bus_removal no-op guard. F6 (pub decrypt_bus) left by-design.
Relocate AACS bus decryption to one spot at the bottom of the sector chain:
the Drive's SectorSource impl applies decrypt_bus_in_content (gated to
encrypted-content ranges), with the stage decided once from the unlock/
handshake result (BusStage::from_read_data_key). Remove read_data_key from
DecryptKeys::Aacs and AacsState and the two downstream decrypt_bus call sites;
every reader (key sampler, mux, unit-key validation) now sees already-de-bussed
content and never reasons about bus encryption. Add BusRemovalSectorSource
stream + tests; keep the AacsBusKeyUnavailable fail-loud gate.
The PlayItem STN (stream-number) table offset was hard-coded to 32, which
is only correct for a single-angle PlayItem. A multi-angle PlayItem inserts
number_of_angles(1) + flags(1) + (number_of_angles-1) angle references (10
bytes each) right after still_time, so its STN begins at 32 + 2 +
(number_of_angles-1)*10. The fixed offset lands inside that angle block and
misreads the stream counts as garbage (e.g. n_video=51 on Spider-Man 3 UHD
00245/00246.mpls, number_of_angles=2 => real offset 44), dropping every
stream including the primary video. That makes DiscTitle::has_video() return
false, which drops a multi-angle/seamless-branch feature from the disc's nav
candidate set and loses it in main-title selection.
Compute the STN offset from is_multi_angle (bit 4 of item[10]) + number_of
_angles. Add a regression test building a 2-angle PlayItem and asserting the
video stream is found past the angle block.
Validated on a 92-disc real UHD/BD corpus (offline scan): 0 main-feature
pick changes vs baseline, 3 multi-angle titles (Spider-Man 3 244/245/246)
recover has_video. Adds examples/corpus_titledump.rs as the regression tool.
The fallback composite gate in rank_titles flagged title P as a "play-all
wrapper" whenever some other title Q's clip set was a proper subset of P's,
Q had video, and Q.size >= 0.5 * P.size. main_feature_order then ranks
standalone-before-composite, demoting P below the decoy Q.
On a seamless-branch feature this inverts the pick: the real 00800 (2h03,
4 clips = body + intro/credits/seamless segments) is a SUPERSET of the
single-clip body decoy 00700 (1h50), and 700 >= 50% of 800's size, so 800
was mis-flagged composite and lost to the shorter 700 — the wrong title
gets ripped.
Add a duration-first wrapper-shape gate: P is only a composite when the
subset Q is EITHER a much shorter cut (Q.duration < 0.85 * P.duration, a
concat/play-all padding Q's runtime) OR nearly size-equal (Q.size >= 0.90 *
P.size, a bumper/outro wrapper). A near-full-length but markedly smaller
body subset (Q runs ~0.89 of P's duration yet holds far fewer bytes,
because P's branch segments add bytes not runtime) is a real seamless
branch, so P is kept. Uses per-title duration_secs/size_bytes, stays
capacity-independent and feature-gate-safe.
Preserves the real play-all avoidance: the 00245 bumper-wrapper decoy
(Q ~0.96 of both duration and size) stays demoted via the size limb, and
a concat play-all (subset < half the runtime) via the duration limb.
Add regression tests: seamless_branch_feature_not_demoted_issue_45 and
true_play_all_concat_wrapper_still_demoted. All existing selection tests
pass.
Bring every flagged comment block within its cap (inline // <=3 lines,
/// item-doc prose <=8): tighten wording, or convert an over-long inline
block that documents the item below it into a /// doc, preserving all
technical facts. No code, string literals, or logic changed.
Probe dir_is_case_insensitive against the real target volume by creating
the dir first. Previously the test probed a not-yet-created dir, which
returns a conservative true; that disagreed with extract_tree's own
post-mkdir probe on a case-sensitive host (Linux CI), panicking the test.
Both branches (collision on insensitive, clean coexist on sensitive) still
verified.
- decrypt.rs: three refs to the #[cfg(test)]-only decrypt_sectors_mapped
wrapper can't resolve in a non-test doc build; de-link to inline code
(keeps the name, no broken link).
- Fix DecryptingSectorSource::with_content_ranges link to its full
crate::sector path so it resolves.
READ CAPACITY is the only authoritative whole-disc size for an image
(the UDF partition is a subset — measured 288 sectors / ~576 KB short on
a real BD, which would truncate the backup anchor). Its field failures
are sporadic (5/5 reliable on the target drive), so retry it a few times
rather than substituting a short value. On persistent failure the shared
scan/identify path stays lenient (capacity 0; MKV reads by extent) and
image_read_sectors() returns EmptyImage so an ISO fails cleanly instead
of writing a truncated or empty file.
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.