Commit Graph
1383 Commits
Author SHA1 Message Date
Matthew Jackson 841328af0f Merge #45 failsafe: chapter-gated composite bumper limb + lowest-id tiebreak 2026-09-20 19:36:33 -07:00
Matthew Jackson a35f336c52 Merge #52: PGS BlockDuration + HEVC NAL guard 2026-09-20 19:36:33 -07:00
Matthew Jackson 00f590f634 mux: guarantee a BlockDuration on every subtitle block (#52)
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.
2026-09-20 19:33:25 -07:00
Matthew Jackson 827eaf0452 disc: gate composite bumper limb on a complete chapter table (#45)
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.
2026-09-20 19:32:39 -07:00
Matthew Jackson f4ea6ea129 aacs/keysource: de-conflate "no entry" vs matched-but-underivable (#46)
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.
2026-09-20 19:16:06 -07:00
Matthew Jackson b47bccce7b libfreemkv 1.7.4: codeaudit fixes (v1.7.0..HEAD sweep)
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.
v1.7.4-rc2
2026-09-19 16:19:48 -07:00
Matthew Jackson 13b93f803b libfreemkv: fix broken intra-doc links to BusStage::Passthrough in drive/mod.rs (doc-links gate) v1.7.4-rc1 2026-09-19 14:25:13 -07:00
Matthew Jackson 1894fa1a49 libfreemkv 1.7.4: single drive-owned AACS bus-removal point
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.
2026-09-19 13:59:52 -07:00
Matthew Jackson 5696175520 restore freemkv-unlock path dep for local dev (post-v1.7.3) v1.7.3-rc2 2026-09-19 05:31:13 +00:00
Matthew Jackson e99cb95bd4 v1.7.3: bump version (freemkv-unlock git-pinned for the tag) v1.7.3 v1.7.3-rc1 2026-09-19 05:31:10 +00:00
Matthew Jackson 1794f5c406 merge: record v1.7.2 release into dev (-s ours; keep path deps + dev's next-cycle work) v1.7.1-rc8 2026-09-18 21:39:15 -07:00
Matthew Jackson 9379507c15 docs: backtick item[10]/item[0..5] in mpls module doc so rustdoc doesn't read them as intra-doc links (doc-links CI) 2026-09-18 21:30:25 -07:00
Matthew Jackson 8457efb5da changelog: 1.7.3 — multi-angle MPLS STN fix (#45) + read_structure_files for --share 2026-09-18 21:11:59 -07:00
Matthew Jackson dbc135bee0 disc: add read_structure_files() to capture BDMV/DVD index metadata for --share diagnostics 2026-09-18 21:08:28 -07:00
Matthew Jackson 48fb9b2cf6 mpls: locate STN table past the multi-angle angle block (issue #45)
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.
2026-09-18 16:38:45 -07:00
Matthew Jackson 013f27eede fix(disc): SAFETY — spare seamless-branch feature from wrapper-composite demotion (#45)
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.
2026-09-18 13:34:50 -07:00
Matthew Jackson 9f0e10a524 restore freemkv-unlock path dep for local dev (post-v1.7.2) v1.7.2-rc5 2026-09-18 18:37:24 +00:00
Matthew Jackson b65124728c v1.7.2: bump version (freemkv-unlock git-pinned for the tag) v1.7.2 v1.7.2-rc4 2026-09-18 18:37:21 +00:00
Matthew Jackson 10d073286c changelog: 1.7.2 — record the SET data-phase unlock fix + READ CAPACITY/empty-ISO hardening v1.7.1-rc7 2026-09-18 06:55:00 -07:00
Matthew Jackson 89fd1a6e4d merge: record v1.7.1 main into dev (dev already at 1.7.1; keep path deps) v1.7.1-rc6 2026-09-17 23:08:09 -07:00
Matthew Jackson 5c9e2fefe5 comment-guard: trim over-cap comment blocks to caps (comments only, no code change)
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.
2026-09-17 22:47:32 -07:00
Matthew Jackson 041ac5876f test: make case-collision test host-FS-independent
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.
2026-09-17 22:37:17 -07:00
Matthew Jackson e0875b630c docs: fix broken intra-doc links
- 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.
2026-09-17 22:37:17 -07:00
Matthew Jackson d5902ff9f9 deps: sha1 0.10→0.11 (collapse RustCrypto digest/block-buffer/crypto-common dup); drop unused rand 2026-09-17 22:18:07 -07:00
Matthew Jackson 01cd85f514 changelog: open 1.7.2 — UNRELEASED (unified release; firmware ABI v2 in freemkv-unlock) 2026-09-17 14:35:17 -07:00
Matthew Jackson 6de61484a3 docs: point the freemkv-unlock path-dep note at release-orchestrate.yml (release.sh is gone) 2026-09-17 14:24:49 -07:00
Matthew Jackson 764519de43 aacs/disc: test coverage for VID key-derivation + handshake route selection; diag: print unknown mkb_version 2026-09-15 20:37:18 -07:00
Matthew Jackson 2fb7d78b29 libfreemkv: retry READ CAPACITY for ISO sizing; hard-error if unavailable
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.
2026-09-15 16:45:48 -07:00
Matthew Jackson 18024ea5f5 scsi/macos: fix io_service_t leak, honor bsd_name in fallback, fix FFI doc
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.
2026-09-15 14:40:54 -07:00
Matthew Jackson 2287bb4449 labels/pes/disc: overflow + robustness fixes
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).
2026-09-15 14:40:47 -07:00
Matthew Jackson 013cc3ce8d net/error/hex: NAT64 SSRF arm, Dir Display arms, reject leading +
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.
2026-09-15 14:40:40 -07:00
Matthew Jackson ab2416d07a aacs: propagate MKB walk errors, add bounds/truncation guards, fix FMTS carry
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.
2026-09-15 14:40:33 -07:00
Matthew Jackson b735fb3730 mux/codec: PTS carry, buffer cap, EOS core drop, PPS-by-id, dead code
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.
2026-09-15 14:40:26 -07:00
Matthew Jackson f1b7a73b93 mux: harden MKV child loops, read real mp4 sample rate, fix pre-video PTS origin
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.
2026-09-15 14:40:18 -07:00
Matthew Jackson fbe91895c4 mux: fix filtered-export drop gate, log skipped key-map, restore json fields
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.
2026-09-15 14:40:09 -07:00
Matthew Jackson 679894e5ea aacs/disc/drive: fix bus-key gate credit, shift guard, tray-unlock log
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).
2026-09-15 14:39:53 -07:00
Matthew Jackson b7914c5995 decrypt: honor with_content_ranges in the mapped AACS read path
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).
2026-09-15 14:39:31 -07:00
Matthew Jackson 747aeba4a4 error: drop premature E7031 KeyServiceConfig variant
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.
2026-09-15 13:46:06 -07:00
Matthew Jackson 034a8b145b fix: cross-repo integration — widen decrypt_bus, SSRF parity, KeyServiceConfig
- 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.
2026-09-15 13:36:47 -07:00
Matthew Jackson 6720ccdf8b style: cargo fmt the audit changes 2026-09-15 11:02:53 -07:00
Matthew Jackson 24ab408559 test/labels: cover dirimage nesting cap, long_ad dir, paramount feature hint
- 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.
2026-09-15 11:02:04 -07:00
Matthew Jackson 6e427e2eac test(scsi/linux): cover the fd-reopen path helpers
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).
2026-09-15 10:58:38 -07:00
Matthew Jackson e130528fea test: cover drive tray/speed rejection, bdnav Call ops, mkv interlace patch
- 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.
2026-09-15 10:56:26 -07:00
Matthew Jackson 0de2e598ef dvdnav(test): cover Vm set-ops (incl divide/mod by zero) and First-Play SetSystem
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.
2026-09-15 10:51:23 -07:00
Matthew Jackson d0a5a90261 cleanup: remove dead MOBJ cmd cap, add early-break and doc clarifications
- 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).
2026-09-15 10:49:16 -07:00
Matthew Jackson be296b3901 mux(timeline): tag provisionals with a monotonic epoch counter
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.
2026-09-15 10:45:35 -07:00
Matthew Jackson b30b06e04e mpls/ifo: fix STN gate off-by-one and pin the PGC palette chroma order
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.
2026-09-15 10:42:37 -07:00
Matthew Jackson e6d86b3582 labels: fix fox hint field disagreement and deluxe bare studio anchor
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.
2026-09-15 10:38:40 -07:00
Matthew Jackson 2a701a217a dirimage: refuse a read that begins past the image's capacity
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.
2026-09-15 10:33:26 -07:00
Matthew Jackson 90e8e99152 mux(resolve): surface the reopen error instead of swallowing it
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.
2026-09-15 10:33:26 -07:00