Files
libfreemkv/docs/bluray.md
T
Matthew Jackson 6e14fc0c0c libfreemkv: 1.7.0 groundwork — sector-recycle soundness + CI hardening
Security
- Remove the unsafe Vec::set_len over recycled sector buffers — UB reachable
  from safe code (GHSA-j8ww-f5fg-9pmh, low). Uses Vec::resize; pooling unchanged.

CI / tooling
- clippy on stable with the MSRV single-sourced from Cargo.toml; cargo-deny
  dependency-audit gate with first-party git sources allowed (#15); audience-based
  comment-guard (#17).

Docs
- README (keydb ownership, BD-J parser count), drive-access transport/reset
  notes, mux timeline module-doc links.
2026-09-02 12:40:55 -07:00

12 KiB

src/disc/bluray.rs — extended notes

Overflow prose for comments that were trimmed to the internal 3-line cap in src/disc/bluray.rs. Each section is pointed to by a // See docs/bluray.md — <topic> comment at the corresponding site.

CLIP_STREAM_EXTS

Stream-file extensions probed for a BD-family playlist clip, in priority order. A clip is normally .m2ts; AACS 2.1 (FMTS) discs name the main feature .fmts (an M2TS transport stream plus forensic variant segments) and 3D discs use .ssif. .m2ts is tried first, so a normal clip is unaffected — the fallback only runs when .m2ts is absent (exactly when file_extents errors).

Scope: these are all variants that live in BDMV/STREAM/ and are reached through an MPLS playlist. HD-DVD's .evo does NOT belong here — HD-DVD is a different tree (HVDVD_TS/) with .XPL playlists and needs its own enumerator (a peer to parse_playlist), not another extension in this list.

scan_bluray_titles

Cancellation: halt is polled between playlists and once more after the loop, and a read that fails with Error::Halted — how a live drive reports a Stop, since Drive::checked_exec fails every command once its flag is set and Drive::read preserves the variant — is propagated rather than swallowed. Halted is the only error this returns from the enumeration itself; an unreadable or unparseable playlist keeps its best-effort skip.

It has to be an error and not a short title list, for the same reason spelled out on Disc::scan_hddvd_titles: a cancelled enumeration that returned Ok would be indistinguishable from a disc that genuinely holds fewer titles, and the caller would cache and act on it. Before this, a Stop pressed mid-scan failed every remaining .mpls read in turn, each one silently skipped, and the scan returned Ok(truncated) at rc=0.

parse_playlist

Sums PlayItem durations; returns Ok(None) if the playlist is under 30 seconds (skips menu / clip-info stub playlists), fails to parse, or names a clip that cannot be resolved. Physical sector extents are pulled from the UDF allocation descriptors of each referenced .m2ts (deduplicated by clip_id).

Ok(None) keeps its two benign meanings (unparseable MPLS, sub-30s playlist) plus the deliberate "drop this title" outcomes below. Err means the scan is over: today that is only Error::Halted, the operator's Stop, which must not be reported as a disc that simply holds fewer titles.

Test: parse_playlist_keeps_exactly_30_seconds

At exactly 30s the playlist is kept (< 30.0 is strict). The fixture is a fully wired BDMV (make_bdmv_fs), not the bare make_min_fs this used to use. make_min_fs lays no STREAM/CLIPINF, so the play item's .clpi read now fails and the title drops — which is correct behaviour for an unresolvable clip but has nothing to do with the 30-second boundary this test exists to pin. Wiring the clip keeps the test measuring exactly one thing.

make_bdmv_fs_ssif

Full BDMV with a real Blu-ray 3D layout: .ssif files under BDMV/STREAM/SSIF/<clip>.ssif (note the SSIF subdirectory, unlike make_bdmv_fs_ext) plus a matching .clpi in CLIPINF. Resolving the SSIF is what latches is_3d = true in parse_playlist.

Test: parse_playlist_fmts_clip_resolves_extent

AACS 2.1: the feature clip is 00001.fmts, NOT .m2ts. The CLIP_STREAM_EXTS fallback in parse_playlist must still resolve the physical extent — before the fix the hard-coded .m2ts path errored, yielding empty extents (a silent empty rip and 0 encrypted samples for key resolution). Size still comes from the .clpi, which parses regardless.

Test: parse_playlist_dedups_repeated_clip_extents_and_size

THE 0.31.0 DEDUP PATH. A playlist that references the SAME clip_id from multiple PlayItems (seamless split / looped segment) must count the physical extents and packet bytes exactly once — mux reads extents in order, so a duplicate would mux the A/V twice and inflate size_bytes (bluray.rs: first_ref = seen_clips.insert(...) gates both total_size += and the extents.push). Per-PlayItem Clip entries are still recorded for both.

Test: parse_playlist_missing_clpi_yields_no_title

A clip whose .clpi is missing must yield no title.

This test used to assert the opposite — a title with size_bytes == 0 and empty extents — and its docstring quoted the buggy control flow ("bluray.rs only fetches extents inside the if let Ok(clpi_data)") as if it were the specification. It was blessing the defect: the title's duration_secs is summed from the PlayItems before the clip is resolved, so the returned title advertised the movie's full runtime while carrying none of its bytes, and the discarded error meant not one log line said so. That is the flagship failure class of this crate — a failure that looks like success — reached through an ordinary missing or scratched CLIPINF file.

The correct behaviour is the same as for a clip whose extents cannot be resolved (see parse_playlist_unreadable_clip_icb_yields_no_title): drop the title and log the read's own error code.

That last clause is now asserted, not merely asked for. The site's comment insists on e.code() because a missing .clpi (E6003), a scratched one (E6000) and a malformed one (E6002) are different populations, but nothing checked it: putting a literal back compiled and passed. Mutation: "E6017" (or any fixed code) in place of "E{}", e.code() fails here, and so does dropping the warn entirely — a silent drop is the same invisible title loss this test was written for, one step later.

Test: parse_playlist_unrecorded_extent_yields_no_title

A clip stream whose ICB declares an UNRECORDED (ECMA-167 4/14.14.1.1 type-1) extent must not yield a title at all.

The extent is allocated to the file but was never written, so the file's content there is zeros while the media holds whatever was left at those sectors. Neither answer a (lba, sector_count) read plan can give is true — reading it splices undefined sectors into the rip as content, dropping it slides every later extent's byte space — so the title is refused rather than mis-ripped. This fixture is the shape a crafted disc uses to get such a range into a title's extent list.

(The sectors > 0 && lba > 0 filter below the resolver stays as defence in depth; a zero-length AD is only reachable as an unrecorded descriptor, since a zero-length TYPE 0 one terminates the AD list.)

Test: parse_playlist_unreadable_clip_icb_yields_no_title

An unrecorded extent was never the only way a clip fails to resolve.

RED BEFORE GREEN: this fixture gives the .m2ts an ICB whose descriptor tag is neither 261 nor 266, so file_extents returns DiscRead — the same variant a scratched sector under a real clip's ICB produces, which is the ordinary way this happens on real media. Before the fix only UdfUnrecordedExtent set the drop flag, so this fell through to the "file absent" path and parse_playlist returned a title: full declared duration from the play item, total_size already counted from the .clpi, and zero extents — a movie advertising its runtime with the content missing, and not one log line. The clip must drop the title exactly as an unrecorded extent does.

Test: parse_playlist_logs_a_non_absence_ssif_failure_the_m2ts_fallback_hid

A non-absence SSIF failure that the .m2ts fallback then papers over must be logged, with its own code.

unresolved had exactly one reader — if let (None, Some(code)) — so a code recorded for /BDMV/STREAM/SSIF/<clip>.ssif was thrown away whenever the fallback succeeded. The disc here IS a 3D disc: the SSIF is present and carries both eyes, and only its ICB is unreadable (a scratched sector, the ordinary way this happens). The title shipped base-view 2D at rc=0 with is_3d false and not one log line, so the operator's rip is silently missing the dependent view and the journal cannot tell this disc apart from one that was only ever 2D.

The title is deliberately still returned — the base view resolved, so refusing would trade a degraded rip for no rip. The defect being fixed is the silence, not the fallback.

Mutations this catches: deleting the new (Some(_), Some(code)) arm, or restoring the if let (None, Some(code)) shape, leaves no event to find; logging a fixed code instead of e.code() fails the code assertion; making the arm return Ok(None) fails the title assertion.

Test: parse_playlist_id_falls_back_to_zero_when_suffix_is_not_mpls

A filename that is long enough (>= 5 bytes) but does NOT end in ".mpls" must NOT have its last 5 bytes stripped — the whole string is handed to the numeric parse instead, which fails and falls back to playlist_id 0 (bluray.rs filename.len() >= 5 && filename[len-5..].eq_ignore_ascii_case(".mpls")).

Test: scan_bluray_titles_skips_non_mpls_extension_file

A non-directory PLAYLIST entry whose name does NOT end in ".mpls" must be skipped even though its content parses as a perfectly good (long) MPLS playlist — extension gating, not content sniffing, decides eligibility (bluray.rs !entry.is_dir && entry.name...ends_with(".mpls")).

HaltingReader

A SectorSource that fails every read in halt_range with Error::Halted — exactly how a live drive behaves once the operator presses Stop: Drive::checked_exec fails every SCSI command with Halted from then on, and Drive::read deliberately preserves the variant. Reads outside the range still succeed, so a test can aim the cancel at one structure and leave the scan far enough along to have something to truncate.

Test: halted_playlist_read_is_not_reported_as_a_shorter_disc

A Stop on a live drive never touches ScanOptions::halt: Drive has its own flag and checked_exec fails every SCSI command with Error::Halted once it is set. The Blu-ray enumerator must not swallow that into a successful scan.

RED BEFORE GREEN: with the propagation reverted this returned Ok([00800]) — the if let Ok(mpls_data) skipped the cancelled read of 00801.mpls, the loop ended, and a half-enumerated disc came back at success. One title from a two-title disc is indistinguishable from a disc that genuinely holds one title, and the caller caches and rips from it.

The halt lands on the second playlist deliberately: it is the last iteration, so nothing after it would poll a flag — only propagating the read's own error catches it.

Test: halted_clpi_read_is_not_accounted_as_an_unresolvable_clip

The same cancel landing on a .clpi read must not be classified as an unresolvable clip either.

RED BEFORE GREEN: with the Err(Error::Halted) arm removed from the CLIPINF match, the cancel fell into the generic "clip could not be resolved" arm, which logs a disc-defect code and drops the title — accounting an operator Stop as a scratched disc, and (in the scan loop) dropping every remaining playlist in turn for a truncated Ok. With the fix the cancel is propagated with its own variant intact.

Test: halted_extent_resolve_is_not_a_title_missing_its_clip

And the same cancel landing on file_extents — the clip's ICB, not its CLIPINF — must propagate too.

RED BEFORE GREEN: note used to exempt Halted from the unresolved classification (correctly — a cancel is not an authoring hole) but had no way to propagate it, so the resolver simply produced no extents and parse_playlist returned a title with the clip's runtime counted, its size_bytes counted from the .clpi, and zero bytes behind it. Measured with the propagation reverted: Ok(Some((768000, []))) — the flagship defect shape, wearing a cancel.

Test: read_meta_title_ignores_non_xml_file_regardless_of_content

A non-.xml file must be ignored even if its content looks like a valid meta XML (contains a <di:name>) — extension gating, not content sniffing, decides eligibility (bluray.rs !e.is_dir && e.name...ends_with(".xml")).