Sweep the pinned toolchain to Rust 1.97

The Windows UI needs current winsafe, whose real minimum is 1.89 (its manifest
under-declares 1.87 while it uses NonNull::from_ref). Rather than stop at the
minimum, this goes to current stable and fixes what that costs.

The counter-intuitive result: 1.97 is CHEAPER than 1.89. libfreemkv had 54
clippy errors at 1.89 and 6 at 1.97, because clippy tightened the noisy
collapsible_if lint in between. Stopping at the minimum would have been the most
expensive choice available.

Roughly 47 lints across the eight repos, the large majority auto-fixed:
libfreemkv 6, freemkv-engine 14, bdemu 8, freemkv-keysources 7, autorip 6,
freemkv-unlock 3, freemkv-i18n 3. The hand-fixed ones are a descending sort to
sort_by_key(Reverse), four manual checked-division sites, a loop counter replaced
by enumerate, and a loop whose first let-else became a while-let.

Worth recording for whoever bumps next: clippy is MSRV-AWARE. Those 54 lints only
appear once the crate DECLARES 1.89 or later, because let-chains become
available. A bare `cargo +1.89 clippy` against a manifest still pinned at 1.87
reports clean and is meaningless — gate with the real precommit script, which is
also the only thing that covers build scripts.

The pin still sits below the Mac default, so it keeps doing its job: catching
lint drift locally before CI sees it.
This commit is contained in:
Matthew Jackson
2026-07-29 21:00:55 -07:00
parent a32373ff40
commit 5f8dc392c0
48 changed files with 444 additions and 452 deletions
+4 -4
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.87.0 - uses: dtolnay/rust-toolchain@1.97.0
with: with:
components: clippy, rustfmt components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.87.0 - uses: dtolnay/rust-toolchain@1.97.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo test --tests - run: cargo test --tests
@@ -33,7 +33,7 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.87.0 - uses: dtolnay/rust-toolchain@1.97.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo check - run: cargo check
@@ -41,7 +41,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.87.0 - uses: dtolnay/rust-toolchain@1.97.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
# Build the tests (not just `cargo check`): catches errors in test # Build the tests (not just `cargo check`): catches errors in test
# code and forces full codegen of the Windows-only SPTI transport # code and forces full codegen of the Windows-only SPTI transport
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
# Tests run as a PARALLEL TRIPWIRE: they fail the run if they fail, but the # Tests run as a PARALLEL TRIPWIRE: they fail the run if they fail, but the
# publish/release jobs do NOT `needs:` this job. The tag decision was already # publish/release jobs do NOT `needs:` this job. The tag decision was already
# gated by the local precommit (same Rust 1.87, same commit). Binary consumers # gated by the local precommit (same Rust 1.97, same commit). Binary consumers
# (freemkv/autorip/bdemu) git-tag-pin libfreemkv and therefore start building # (freemkv/autorip/bdemu) git-tag-pin libfreemkv and therefore start building
# the instant this tag exists — so this test job and the crates.io publish # the instant this tag exists — so this test job and the crates.io publish
# below must NOT sit on their critical path. # below must NOT sit on their critical path.
@@ -33,7 +33,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.87.0 - uses: dtolnay/rust-toolchain@1.97.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
# libfreemkv is a library — Cargo.lock isn't tracked, so --locked # libfreemkv is a library — Cargo.lock isn't tracked, so --locked
# would always fail (no lockfile to lock against on a fresh runner). # would always fail (no lockfile to lock against on a fresh runner).
+1 -1
View File
@@ -2,7 +2,7 @@
name = "libfreemkv" name = "libfreemkv"
version = "1.6.0" version = "1.6.0"
edition = "2024" edition = "2024"
rust-version = "1.87" rust-version = "1.97"
license = "MIT" license = "MIT"
description = "Open source raw disc access library for optical drives" description = "Open source raw disc access library for optical drives"
repository = "https://github.com/freemkv/libfreemkv" repository = "https://github.com/freemkv/libfreemkv"
+4 -4
View File
@@ -77,10 +77,10 @@ fn emit_git_suffix() {
// Re-run when HEAD (or the branch it points at) moves so the stamp stays // Re-run when HEAD (or the branch it points at) moves so the stamp stays
// current without a clean rebuild. // current without a clean rebuild.
println!("cargo:rerun-if-changed=.git/HEAD"); println!("cargo:rerun-if-changed=.git/HEAD");
if let Ok(head) = std::fs::read_to_string(".git/HEAD") { if let Ok(head) = std::fs::read_to_string(".git/HEAD")
if let Some(ref_path) = head.strip_prefix("ref: ") { && let Some(ref_path) = head.strip_prefix("ref: ")
println!("cargo:rerun-if-changed=.git/{}", ref_path.trim()); {
} println!("cargo:rerun-if-changed=.git/{}", ref_path.trim());
} }
} }
+2 -1
View File
@@ -41,7 +41,8 @@ pub const ALIGNED_UNIT_SECTORS: u32 = (ALIGNED_UNIT_LEN / SECTOR_BYTES) as u32;
/// underflow wraps to ~2^32 and, because `2^32 ≡ 1 (mod 3)`, mis-reports the /// underflow wraps to ~2^32 and, because `2^32 ≡ 1 (mod 3)`, mis-reports the
/// alignment (e.g. `lba == unit_base - 1` would falsely read as aligned). /// alignment (e.g. `lba == unit_base - 1` would falsely read as aligned).
pub fn is_unit_aligned(lba: u32, unit_base: u32) -> bool { pub fn is_unit_aligned(lba: u32, unit_base: u32) -> bool {
lba.saturating_sub(unit_base) % ALIGNED_UNIT_SECTORS == 0 lba.saturating_sub(unit_base)
.is_multiple_of(ALIGNED_UNIT_SECTORS)
} }
use crate::consts::SECTOR_BYTES; use crate::consts::SECTOR_BYTES;
+2 -2
View File
@@ -49,7 +49,7 @@ pub(crate) fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
/// expansions. /// expansions.
pub(crate) fn aes_cbc_encrypt(key: &[u8; 16], data: &mut [u8]) { pub(crate) fn aes_cbc_encrypt(key: &[u8; 16], data: &mut [u8]) {
debug_assert!( debug_assert!(
data.len() % 16 == 0, data.len().is_multiple_of(16),
"aes_cbc_encrypt requires a block-aligned slice" "aes_cbc_encrypt requires a block-aligned slice"
); );
let cipher = Aes128::new(GenericArray::from_slice(key)); let cipher = Aes128::new(GenericArray::from_slice(key));
@@ -71,7 +71,7 @@ pub(crate) fn aes_cbc_encrypt(key: &[u8; 16], data: &mut [u8]) {
pub(crate) fn aes_cbc_decrypt(key: &[u8; 16], data: &mut [u8]) { pub(crate) fn aes_cbc_decrypt(key: &[u8; 16], data: &mut [u8]) {
debug_assert!( debug_assert!(
data.len() % 16 == 0, data.len().is_multiple_of(16),
"aes_cbc_decrypt requires a block-aligned slice" "aes_cbc_decrypt requires a block-aligned slice"
); );
let cipher = Aes128::new(GenericArray::from_slice(key)); let cipher = Aes128::new(GenericArray::from_slice(key));
+2 -4
View File
@@ -328,10 +328,8 @@ fn parse_program_info(data: &[u8]) -> Vec<ClpiStream> {
} }
} }
// PG, IG: coding_type + 3-byte language [+ char_code for PG] // PG, IG: coding_type + 3-byte language [+ char_code for PG]
c::PG | c::IG => { c::PG | c::IG if sci.len() >= 4 => {
if sci.len() >= 4 { language = String::from_utf8_lossy(&sci[1..4]).to_string();
language = String::from_utf8_lossy(&sci[1..4]).to_string();
}
} }
_ => {} _ => {}
} }
+13 -13
View File
@@ -249,10 +249,10 @@ fn crack_key_scan(
while i < ext.sector_count && tried < max_tries { while i < ext.sector_count && tried < max_tries {
// Cooperative cancellation — poll once per batch, the same cadence // Cooperative cancellation — poll once per batch, the same cadence
// sweep/patch use, so a Stop / watchdog can interrupt the scan. // sweep/patch use, so a Stop / watchdog can interrupt the scan.
if let Some(h) = halt { if let Some(h) = halt
if h.is_cancelled() { && h.is_cancelled()
break 'outer; {
} break 'outer;
} }
// Liveness beacon: a long scan over a damaged disc stays visible. // Liveness beacon: a long scan over a damaged disc stays visible.
// The heartbeat is time-throttled; only when it actually beats do // The heartbeat is time-throttled; only when it actually beats do
@@ -367,16 +367,16 @@ pub fn descramble_region(buf: &mut [u8], title_key: &mut [u8; 5]) {
original.copy_from_slice(chunk); original.copy_from_slice(chunk);
} }
lfsr::descramble_sector(title_key, chunk); lfsr::descramble_sector(title_key, chunk);
if let Some(crib) = crib { if let Some(crib) = crib
if chunk[0x80..0x80 + 10] != crib[..] { && chunk[0x80..0x80 + 10] != crib[..]
// Cached key is stale for this region — restore the ciphertext and {
// crack this sector's own key. // Cached key is stale for this region — restore the ciphertext and
chunk.copy_from_slice(&original); // crack this sector's own key.
if let Some(fresh) = stevenson::crack_title_key(chunk) { chunk.copy_from_slice(&original);
*title_key = fresh; if let Some(fresh) = stevenson::crack_title_key(chunk) {
} *title_key = fresh;
lfsr::descramble_sector(title_key, chunk);
} }
lfsr::descramble_sector(title_key, chunk);
} }
} }
} }
+5 -5
View File
@@ -332,11 +332,11 @@ impl AacsKeyMap {
if sectors == 0 { if sectors == 0 {
return; return;
} }
if let Some(last) = plan.last_mut() { if let Some(last) = plan.last_mut()
if last.start_lba.saturating_add(last.sector_count) == lba { && last.start_lba.saturating_add(last.sector_count) == lba
last.sector_count += sectors; {
return; last.sector_count += sectors;
} return;
} }
plan.push(crate::disc::Extent { plan.push(crate::disc::Extent {
start_lba: lba, start_lba: lba,
+64 -65
View File
@@ -29,12 +29,11 @@ impl Disc {
for entry in &playlist_dir.entries { for entry in &playlist_dir.entries {
if !entry.is_dir && entry.name.to_lowercase().ends_with(".mpls") { if !entry.is_dir && entry.name.to_lowercase().ends_with(".mpls") {
let path = format!("/BDMV/PLAYLIST/{}", entry.name); let path = format!("/BDMV/PLAYLIST/{}", entry.name);
if let Ok(mpls_data) = udf_fs.read_file(reader, &path) { if let Ok(mpls_data) = udf_fs.read_file(reader, &path)
if let Some(title) = && let Some(title) =
Self::parse_playlist(reader, udf_fs, &entry.name, &mpls_data) Self::parse_playlist(reader, udf_fs, &entry.name, &mpls_data)
{ {
titles.push(title); titles.push(title);
}
} }
} }
} }
@@ -92,54 +91,54 @@ impl Disc {
let mut pkt_count: u32 = 0; let mut pkt_count: u32 = 0;
let clpi_path = format!("/BDMV/CLIPINF/{}.clpi", play_item.clip_id); let clpi_path = format!("/BDMV/CLIPINF/{}.clpi", play_item.clip_id);
if let Ok(clpi_data) = udf_fs.read_file(reader, &clpi_path) { if let Ok(clpi_data) = udf_fs.read_file(reader, &clpi_path)
if let Ok(clip_info) = clpi::parse(&clpi_data) { && let Ok(clip_info) = clpi::parse(&clpi_data)
pkt_count = clip_info.source_packet_count; {
pkt_count = clip_info.source_packet_count;
// Mark the clip seen ONLY after its .clpi parses — a transient // Mark the clip seen ONLY after its .clpi parses — a transient
// read/parse failure on the first PlayItem referencing a clip // read/parse failure on the first PlayItem referencing a clip
// must not permanently suppress its extents/size for a later // must not permanently suppress its extents/size for a later
// PlayItem referencing the same clip that succeeds. // PlayItem referencing the same clip that succeeds.
let first_ref = seen_clips.insert(play_item.clip_id.clone()); let first_ref = seen_clips.insert(play_item.clip_id.clone());
// Only fetch/push the physical extents and add to the // Only fetch/push the physical extents and add to the
// total size the first time this clip_id is seen. // total size the first time this clip_id is seen.
if first_ref { if first_ref {
total_size += pkt_count as u64 * 192; total_size += pkt_count as u64 * 192;
// Get stream file extents from UDF allocation descriptors. // Get stream file extents from UDF allocation descriptors.
// Dual-layer discs split files across layers — UDF knows the real layout. // Dual-layer discs split files across layers — UDF knows the real layout.
// //
// The clip's stream file is normally `.m2ts`, but AACS 2.1 // The clip's stream file is normally `.m2ts`, but AACS 2.1
// (FMTS) discs name the main feature `.fmts` and 3D discs // (FMTS) discs name the main feature `.fmts` and 3D discs
// use `.ssif` (see [`CLIP_STREAM_EXTS`]). A normal `.m2ts` // use `.ssif` (see [`CLIP_STREAM_EXTS`]). A normal `.m2ts`
// clip is unchanged — the fallback only runs when `.m2ts` // clip is unchanged — the fallback only runs when `.m2ts`
// is absent, which is exactly when `file_extents` errors. // is absent, which is exactly when `file_extents` errors.
// 3D discs interleave the left (base) and right (MVC // 3D discs interleave the left (base) and right (MVC
// dependent) views in STREAM/SSIF/<clip>.ssif — note the // dependent) views in STREAM/SSIF/<clip>.ssif — note the
// SSIF/ subdir. Prefer it when present: the SSIF is one // SSIF/ subdir. Prefer it when present: the SSIF is one
// transport stream carrying BOTH eyes on distinct PIDs, // transport stream carrying BOTH eyes on distinct PIDs,
// so muxing it captures the full 3D. 2D clips fall back to // so muxing it captures the full 3D. 2D clips fall back to
// the base .m2ts / .fmts as before. // the base .m2ts / .fmts as before.
let ssif = format!("/BDMV/STREAM/SSIF/{}.ssif", play_item.clip_id); let ssif = format!("/BDMV/STREAM/SSIF/{}.ssif", play_item.clip_id);
let file_exts = match udf_fs.file_extents(reader, &ssif) { let file_exts = match udf_fs.file_extents(reader, &ssif) {
Ok(exts) => { Ok(exts) => {
is_3d = true; is_3d = true;
Some(exts) Some(exts)
} }
Err(_) => CLIP_STREAM_EXTS.iter().find_map(|ext| { Err(_) => CLIP_STREAM_EXTS.iter().find_map(|ext| {
let path = format!("/BDMV/STREAM/{}.{}", play_item.clip_id, ext); let path = format!("/BDMV/STREAM/{}.{}", play_item.clip_id, ext);
udf_fs.file_extents(reader, &path).ok() udf_fs.file_extents(reader, &path).ok()
}), }),
}; };
if let Some(file_exts) = file_exts { if let Some(file_exts) = file_exts {
for (lba, sectors) in file_exts { for (lba, sectors) in file_exts {
if sectors > 0 && lba > 0 { if sectors > 0 && lba > 0 {
extents.push(Extent { extents.push(Extent {
start_lba: lba, start_lba: lba,
sector_count: sectors, sector_count: sectors,
}); });
}
} }
} }
} }
@@ -266,23 +265,23 @@ impl Disc {
// optional) but over-claims 3D for those frames. Real 3D main-feature // optional) but over-claims 3D for those frames. Real 3D main-feature
// playlists are single-clip or uniformly 3D, so this is not exercised; // playlists are single-clip or uniformly 3D, so this is not exercised;
// per-clip 3D would need per-clip stream sets (a larger change). // per-clip 3D would need per-clip stream sets (a larger change).
if is_3d { if is_3d
if let Some(base) = streams.iter().find_map(|s| match s { && let Some(base) = streams.iter().find_map(|s| match s {
Stream::Video(v) => Some(v.clone()), Stream::Video(v) => Some(v.clone()),
_ => None, _ => None,
}) { })
let dep_pid = base.pid.wrapping_add(1); {
let have_dep = streams let dep_pid = base.pid.wrapping_add(1);
.iter() let have_dep = streams
.any(|s| matches!(s, Stream::Video(v) if v.pid == dep_pid)); .iter()
if !have_dep { .any(|s| matches!(s, Stream::Video(v) if v.pid == dep_pid));
streams.push(Stream::Video(VideoStream { if !have_dep {
pid: dep_pid, streams.push(Stream::Video(VideoStream {
secondary: true, pid: dep_pid,
label: crate::disc::MVC_DEPENDENT_LABEL.to_string(), secondary: true,
..base label: crate::disc::MVC_DEPENDENT_LABEL.to_string(),
})); ..base
} }));
} }
} }
+4 -4
View File
@@ -104,10 +104,10 @@ fn max_substream_channels(data: &[u8]) -> Option<u8> {
}; };
let start = pos + rel; let start = pos + rel;
let frame = &data[start..]; let frame = &data[start..];
if let Some(ch) = ac3::acmod_channels(frame) { if let Some(ch) = ac3::acmod_channels(frame)
if ch > 0 { && ch > 0
best = Some(best.map_or(ch, |b| b.max(ch))); {
} best = Some(best.map_or(ch, |b| b.max(ch)));
} }
// Advance past this frame by its declared size when that is mappable; // Advance past this frame by its declared size when that is mappable;
// otherwise step 2 bytes past the sync and re-scan for the next one. // otherwise step 2 bytes past the sync and re-scan for the next one.
+18 -17
View File
@@ -165,13 +165,13 @@ impl Disc {
.iter() .iter()
.map(|p| p.size) .map(|p| p.size)
.fold(0u64, |a, b| a.saturating_add(b)); .fold(0u64, |a, b| a.saturating_add(b));
if let Some(available) = available_space(dest) { if let Some(available) = available_space(dest)
if available < required { && available < required
return Err(Error::DirInsufficientSpace { {
required, return Err(Error::DirInsufficientSpace {
available, required,
}); available,
} });
} }
// Create directories up-front so a leaf write never races a missing // Create directories up-front so a leaf write never races a missing
@@ -388,12 +388,12 @@ fn plan_tree(
let child_rel = host_rel.join(&safe); let child_rel = host_rel.join(&safe);
let child_disc = format!("{disc_path}/{}", entry.name); let child_disc = format!("{disc_path}/{}", entry.name);
// Collision: two distinct disc paths → same host path. // Collision: two distinct disc paths → same host path.
if let Some(prev) = seen_hosts.insert(child_rel.clone(), child_disc.clone()) { if let Some(prev) = seen_hosts.insert(child_rel.clone(), child_disc.clone())
if prev != child_disc { && prev != child_disc
return Err(Error::DirNameCollision { {
host: child_rel.to_string_lossy().into_owned(), return Err(Error::DirNameCollision {
}); host: child_rel.to_string_lossy().into_owned(),
} });
} }
if entry.is_dir { if entry.is_dir {
dirs.push(child_rel.clone()); dirs.push(child_rel.clone());
@@ -756,10 +756,11 @@ fn is_windows_reserved(base: &str) -> bool {
} }
let up = base.to_ascii_uppercase(); let up = base.to_ascii_uppercase();
for prefix in ["COM", "LPT"] { for prefix in ["COM", "LPT"] {
if let Some(rest) = up.strip_prefix(prefix) { if let Some(rest) = up.strip_prefix(prefix)
if rest.len() == 1 && matches!(rest.as_bytes()[0], b'1'..=b'9') { && rest.len() == 1
return true; && matches!(rest.as_bytes()[0], b'1'..=b'9')
} {
return true;
} }
} }
false false
+6 -6
View File
@@ -305,12 +305,12 @@ fn collect_es(
} }
} }
PRIVATE_STREAM_1 => { PRIVATE_STREAM_1 => {
if let Some(sub) = pkt.sub_stream_id { if let Some(sub) = pkt.sub_stream_id
if (0xC0..=0xC7).contains(&sub) { && (0xC0..=0xC7).contains(&sub)
let slot = audio.entry(sub).or_default(); {
if slot.len() < EVO_ES_SAMPLE_CAP { let slot = audio.entry(sub).or_default();
slot.extend_from_slice(&pkt.data); if slot.len() < EVO_ES_SAMPLE_CAP {
} slot.extend_from_slice(&pkt.data);
} }
} }
} }
+15 -15
View File
@@ -687,15 +687,15 @@ pub fn chapter_at_offset(
/// The 1-based chapter + movie-time offset an LBA falls in, or `(None, None)` /// The 1-based chapter + movie-time offset an LBA falls in, or `(None, None)`
/// if it isn't inside the title. /// if it isn't inside the title.
fn range_chapter(lba: u32, title: &DiscTitle) -> (Option<u32>, Option<f64>) { fn range_chapter(lba: u32, title: &DiscTitle) -> (Option<u32>, Option<f64>) {
if let Some(byte_offset) = byte_offset_in_title(lba, title) { if let Some(byte_offset) = byte_offset_in_title(lba, title)
if let Some((ch, t)) = chapter_at_offset( && let Some((ch, t)) = chapter_at_offset(
&title.chapters, &title.chapters,
byte_offset, byte_offset,
title.duration_secs, title.duration_secs,
title.size_bytes, title.size_bytes,
) { )
return (Some(ch as u32), Some(t)); {
} return (Some(ch as u32), Some(t));
} }
(None, None) (None, None)
} }
@@ -1726,7 +1726,7 @@ impl Disc {
{ {
Some(t) => { Some(t) => {
let mut v = t.extents.clone(); let mut v = t.extents.clone();
v.sort_by(|a, b| b.sector_count.cmp(&a.sector_count)); v.sort_by_key(|e| std::cmp::Reverse(e.sector_count));
v v
} }
None => Vec::new(), None => Vec::new(),
@@ -3049,15 +3049,15 @@ pub fn detect_max_batch_sectors(device_path: &str) -> u16 {
if let Some(bname) = block_name { if let Some(bname) = block_name {
let sysfs_path = format!("/sys/block/{bname}/queue/max_hw_sectors_kb"); let sysfs_path = format!("/sys/block/{bname}/queue/max_hw_sectors_kb");
if let Ok(content) = std::fs::read_to_string(&sysfs_path) { if let Ok(content) = std::fs::read_to_string(&sysfs_path)
if let Ok(kb) = content.trim().parse::<u32>() { && let Ok(kb) = content.trim().parse::<u32>()
// Convert KB to sectors (1 sector = 2 KB = 2048 bytes) {
let sectors = (kb / 2).min(u16::MAX as u32) as u16; // Convert KB to sectors (1 sector = 2 KB = 2048 bytes)
// Align down to 3 (one aligned unit) let sectors = (kb / 2).min(u16::MAX as u32) as u16;
let aligned = (sectors / 3) * 3; // Align down to 3 (one aligned unit)
if aligned >= MIN_BATCH_SECTORS { let aligned = (sectors / 3) * 3;
return aligned.min(MAX_BATCH_SECTORS); if aligned >= MIN_BATCH_SECTORS {
} return aligned.min(MAX_BATCH_SECTORS);
} }
} }
} }
+6 -7
View File
@@ -42,7 +42,7 @@ const CHUNK_SECTORS: u16 = 1023;
// The alignment requirement above is enforced, not just described. // The alignment requirement above is enforced, not just described.
const _: () = assert!( const _: () = assert!(
CHUNK_SECTORS as u32 % crate::aacs::content::ALIGNED_UNIT_SECTORS == 0, (CHUNK_SECTORS as u32).is_multiple_of(crate::aacs::content::ALIGNED_UNIT_SECTORS),
"probe chunks must be a whole number of AACS aligned units" "probe chunks must be a whole number of AACS aligned units"
); );
@@ -393,12 +393,11 @@ fn verdicts(evidence: &HashMap<u16, TrackEvidence>, conclusive: bool) -> HashMap
/// from the map was never observed and keeps its vendor-derived flag. /// from the map was never observed and keeps its vendor-derived flag.
fn apply_verdicts(title: &mut DiscTitle, verdicts: &HashMap<u16, bool>) { fn apply_verdicts(title: &mut DiscTitle, verdicts: &HashMap<u16, bool>) {
for s in &mut title.streams { for s in &mut title.streams {
if let Stream::Subtitle(sub) = s { if let Stream::Subtitle(sub) = s
if sub.codec == Codec::Pgs { && sub.codec == Codec::Pgs
if let Some(&forced) = verdicts.get(&sub.pid) { && let Some(&forced) = verdicts.get(&sub.pid)
sub.forced = forced; {
} sub.forced = forced;
}
} }
} }
} }
+5 -6
View File
@@ -25,12 +25,11 @@ pub fn find_drives() -> Vec<(String, DriveId)> {
let path = std::path::Path::new(&info.path); let path = std::path::Path::new(&info.path);
match crate::scsi::open(path) { match crate::scsi::open(path) {
Ok(mut transport) => { Ok(mut transport) => {
if let Ok(id) = DriveId::from_drive(transport.as_mut()) { if let Ok(id) = DriveId::from_drive(transport.as_mut())
if !id.raw_inquiry.is_empty() && !id.raw_inquiry.is_empty()
&& (id.raw_inquiry[0] & 0x1F) == SCSI_PERIPHERAL_TYPE_OPTICAL && (id.raw_inquiry[0] & 0x1F) == SCSI_PERIPHERAL_TYPE_OPTICAL
{ {
drives.push((info.path.clone(), id)); drives.push((info.path.clone(), id));
}
} }
} }
Err(_) => { Err(_) => {
+1 -1
View File
@@ -20,7 +20,7 @@ pub fn parse_hex_bytes(s: &str) -> Option<Vec<u8>> {
let bytes = body.as_bytes(); let bytes = body.as_bytes();
// Empty → empty Vec (a legitimately-empty variable-length field); odd length // Empty → empty Vec (a legitimately-empty variable-length field); odd length
// is malformed. (`parse_hex_fixed` enforces a concrete length separately.) // is malformed. (`parse_hex_fixed` enforces a concrete length separately.)
if bytes.len() % 2 != 0 { if !bytes.len().is_multiple_of(2) {
return None; return None;
} }
let mut out = Vec::with_capacity(bytes.len() / 2); let mut out = Vec::with_capacity(bytes.len() / 2);
+4 -4
View File
@@ -133,10 +133,10 @@ where
match rx.recv_timeout(slice) { match rx.recv_timeout(slice) {
Ok(v) => return Ok(v), Ok(v) => return Ok(v),
Err(RecvTimeoutError::Timeout) => { Err(RecvTimeoutError::Timeout) => {
if let Some(h) = halt { if let Some(h) = halt
if h.is_cancelled() { && h.is_cancelled()
return Err(BoundedError::Halted); {
} return Err(BoundedError::Halted);
} }
if Instant::now() >= deadline { if Instant::now() >= deadline {
return Err(BoundedError::Timeout); return Err(BoundedError::Timeout);
+9 -9
View File
@@ -752,15 +752,15 @@ impl<I: Send + 'static, R: Send + 'static> Pipeline<I, R> {
Err(payload) => Err(consumer_panicked(payload)), Err(payload) => Err(consumer_panicked(payload)),
}; };
} }
if let Some(h) = halt { if let Some(h) = halt
if h.is_cancelled() { && h.is_cancelled()
return finish_with_grace( {
handle, return finish_with_grace(
&state, handle,
Duration::from_secs(FINISH_GRACE_SECS), &state,
Error::Halted, Duration::from_secs(FINISH_GRACE_SECS),
); Error::Halted,
} );
} }
if Instant::now() >= deadline { if Instant::now() >= deadline {
return finish_with_grace( return finish_with_grace(
+12 -12
View File
@@ -100,10 +100,10 @@ pub fn parse(reader: &mut dyn SectorSource, udf: &UdfFs) -> Option<DiscMetadata>
// Disc-set position is disc-global; first one we successfully // Disc-set position is disc-global; first one we successfully
// read wins. (All bdmt_*.xml on a given disc carry the same // read wins. (All bdmt_*.xml on a given disc carry the same
// value in practice.) // value in practice.)
if out.disc_number.is_none() { if out.disc_number.is_none()
if let Some(ds) = disc_set { && let Some(ds) = disc_set
out.disc_number = Some(ds); {
} out.disc_number = Some(ds);
} }
} }
@@ -184,20 +184,20 @@ fn extract_title(xml_text: &str) -> Option<String> {
// xml::text already trims its result, so an empty string after // xml::text already trims its result, so an empty string after
// extraction means a genuinely empty element. // extraction means a genuinely empty element.
for tag in ["name", "title"] { for tag in ["name", "title"] {
if let Some(s) = xml::text(xml_text, tag) { if let Some(s) = xml::text(xml_text, tag)
if !s.is_empty() { && !s.is_empty()
return Some(s); {
} return Some(s);
} }
} }
// tableOfContents/titleName: search inside the toc block so we // tableOfContents/titleName: search inside the toc block so we
// don't accidentally pick a stray <titleName> from elsewhere. // don't accidentally pick a stray <titleName> from elsewhere.
if let Some((s, e)) = xml::find_element(xml_text, "tableOfContents", 0) { if let Some((s, e)) = xml::find_element(xml_text, "tableOfContents", 0) {
let block = &xml_text[s..e]; let block = &xml_text[s..e];
if let Some(t) = xml::text(block, "titleName") { if let Some(t) = xml::text(block, "titleName")
if !t.is_empty() { && !t.is_empty()
return Some(t); {
} return Some(t);
} }
} }
None None
+11 -12
View File
@@ -36,10 +36,10 @@ pub fn parse(reader: &mut dyn SectorSource, udf: &UdfFs) -> Option<ParseResult>
// Stream number mapping from playbackconfig.xml // Stream number mapping from playbackconfig.xml
let mut stream_map: HashMap<String, u16> = HashMap::new(); let mut stream_map: HashMap<String, u16> = HashMap::new();
if let Some(pc_data) = super::read_jar_file(reader, udf, "playbackconfig.xml") { if let Some(pc_data) = super::read_jar_file(reader, udf, "playbackconfig.xml")
if let Ok(pc_text) = std::str::from_utf8(&pc_data) { && let Ok(pc_text) = std::str::from_utf8(&pc_data)
parse_playback_config(pc_text, &mut stream_map); {
} parse_playback_config(pc_text, &mut stream_map);
} }
let stream_nums = assign_stream_numbers(&stream_infos, &stream_map); let stream_nums = assign_stream_numbers(&stream_infos, &stream_map);
@@ -189,14 +189,13 @@ fn parse_playback_config(text: &str, map: &mut HashMap<String, u16>) {
if let (Some(stream_id_str), Some(info_id)) = ( if let (Some(stream_id_str), Some(info_id)) = (
xml::text(block, "StreamID"), xml::text(block, "StreamID"),
xml::text(block, "StreamInfo_ID"), xml::text(block, "StreamInfo_ID"),
) { ) && let Ok(stream_num) = stream_id_str.parse::<u16>()
if let Ok(stream_num) = stream_id_str.parse::<u16>() { {
// Stream numbers are 1-based per the apply_labels // Stream numbers are 1-based per the apply_labels
// contract; a mapped 0 is unmatchable and silently // contract; a mapped 0 is unmatchable and silently
// drops the label. Skip it rather than store it. // drops the label. Skip it rather than store it.
if stream_num != 0 { if stream_num != 0 {
map.insert(info_id, stream_num); map.insert(info_id, stream_num);
}
} }
} }
from = end; from = end;
+3 -3
View File
@@ -50,10 +50,10 @@ fn merge(ls: Vec<StreamLabel>, mb: Vec<StreamLabel>) -> Vec<StreamLabel> {
if let Some(mb_match) = mb if let Some(mb_match) = mb
.iter() .iter()
.find(|m| m.stream_type == label.stream_type && m.stream_number == label.stream_number) .find(|m| m.stream_type == label.stream_type && m.stream_number == label.stream_number)
&& label.name.is_empty()
&& !mb_match.name.is_empty()
{ {
if label.name.is_empty() && !mb_match.name.is_empty() { label.name = mb_match.name.clone();
label.name = mb_match.name.clone();
}
} }
} }
// Append any menu_base-only stream (present in mb but not in ls by // Append any menu_base-only stream (present in mb but not in ls by
+7 -7
View File
@@ -114,13 +114,13 @@ fn collect_textfield(
if let Ok(n) = rest.parse::<u16>() { if let Ok(n) = rest.parse::<u16>() {
audios.insert(n, label.to_string()); audios.insert(n, label.to_string());
} }
} else if let Some(rest) = kind_n.strip_prefix("Subtitle") { } else if let Some(rest) = kind_n.strip_prefix("Subtitle")
if let Ok(n) = rest.parse::<u16>() { && let Ok(n) = rest.parse::<u16>()
// Subtitle0 is conventionally the "None / Off" disable {
// button, not an actual subtitle stream. // Subtitle0 is conventionally the "None / Off" disable
if n > 0 { // button, not an actual subtitle stream.
subs.insert(n, label.to_string()); if n > 0 {
} subs.insert(n, label.to_string());
} }
} }
} }
+2 -3
View File
@@ -596,14 +596,13 @@ impl<'a> BindingDecoder<'a> {
// Underneath the args: the object the constructor // Underneath the args: the object the constructor
// operates on. For our pattern it's NewObj(X). // operates on. For our pattern it's NewObj(X).
let receiver = self.stack.pop().unwrap_or(StackVal::Unknown); let receiver = self.stack.pop().unwrap_or(StackVal::Unknown);
if let StackVal::NewObj(name) = receiver { if let StackVal::NewObj(name) = receiver
if name == member.class_name { && name == member.class_name {
self.constructions.push(Construction { self.constructions.push(Construction {
binding_type: name, binding_type: name,
args, args,
}); });
} }
}
} }
// invokevirtual / invokestatic / invokeinterface — pop // invokevirtual / invokestatic / invokeinterface — pop
// args per descriptor, push a return placeholder unless // args per descriptor, push a return placeholder unless
+4 -4
View File
@@ -574,10 +574,10 @@ fn extract(reader: &mut dyn SectorSource, udf: &UdfFs) -> Vec<StreamLabel> {
// so the user sees every track even when only the "interesting" // so the user sees every track even when only the "interesting"
// ones have editorial names. Skips the merge when mpls_universal // ones have editorial names. Skips the merge when mpls_universal
// was itself the chosen parser (its labels ARE the labels). // was itself the chosen parser (its labels ARE the labels).
if name != "mpls_universal" { if name != "mpls_universal"
if let Some(mpls_result) = mpls_universal::parse(reader, udf) { && let Some(mpls_result) = mpls_universal::parse(reader, udf)
fill_gaps_from_mpls(&mut labels, &mpls_result.labels); {
} fill_gaps_from_mpls(&mut labels, &mpls_result.labels);
} }
// CLPI orphan streams: PIDs in /BDMV/CLIPINF/*.clpi ProgramInfo // CLPI orphan streams: PIDs in /BDMV/CLIPINF/*.clpi ProgramInfo
+4 -4
View File
@@ -142,10 +142,10 @@ fn find_feature_playlist(text: &str) -> Option<String> {
let element = &text[start..end]; let element = &text[start..end];
// Prefer name="Feature" explicitly. // Prefer name="Feature" explicitly.
if let Some(name) = xml::attr(element, "name") { if let Some(name) = xml::attr(element, "name")
if name.eq_ignore_ascii_case("Feature") { && name.eq_ignore_ascii_case("Feature")
return Some(element.to_string()); {
} return Some(element.to_string());
} }
// Otherwise pick the one with the most audio streams. Count only // Otherwise pick the one with the most audio streams. Count only
+4 -4
View File
@@ -41,10 +41,10 @@ pub fn parse(_reader: &mut dyn SectorSource, udf: &UdfFs) -> Option<ParseResult>
fn labels_from_filenames(names: &[String]) -> Vec<StreamLabel> { fn labels_from_filenames(names: &[String]) -> Vec<StreamLabel> {
let mut seen: Vec<&'static str> = Vec::new(); let mut seen: Vec<&'static str> = Vec::new();
for name in names { for name in names {
if let Some(code) = filename_lang(name) { if let Some(code) = filename_lang(name)
if !seen.contains(&code) { && !seen.contains(&code)
seen.push(code); {
} seen.push(code);
} }
} }
seen.into_iter() seen.into_iter()
+2 -3
View File
@@ -432,14 +432,13 @@ fn parse_stream_entry(item: &[u8], pos: usize, stream_type: u8) -> Option<(Strea
} }
} }
} }
STREAM_CATEGORY_PG_SUBTITLE => { STREAM_CATEGORY_PG_SUBTITLE
// PG: coding_type(1) + language(3). // PG: coding_type(1) + language(3).
// IG is parsed only to advance spos and is then discarded by the // IG is parsed only to advance spos and is then discarded by the
// caller, so it deliberately has no arm here. // caller, so it deliberately has no arm here.
if sa.len() >= 4 { if sa.len() >= 4 => {
language = String::from_utf8_lossy(&sa[1..4]).to_string(); language = String::from_utf8_lossy(&sa[1..4]).to_string();
} }
}
_ => {} _ => {}
} }
+5 -5
View File
@@ -195,11 +195,11 @@ impl Ac3Parser {
// First access unit that starts in this PES's own bytes: adopt // First access unit that starts in this PES's own bytes: adopt
// this PES's timestamp so a genuine PTS jump is followed instead // this PES's timestamp so a genuine PTS jump is followed instead
// of the running cadence drifting past it. // of the running cadence drifting past it.
if let Some(a) = &anchor { if let Some(a) = &anchor
if start >= a.at { && start >= a.at
frame_pts_ns = a.pts_ns; {
anchor = None; frame_pts_ns = a.pts_ns;
} anchor = None;
} }
let duration_ns = frame_duration_ns(remaining, bsid); let duration_ns = frame_duration_ns(remaining, bsid);
pending = Some(PendingAu { pending = Some(PendingAu {
+11 -11
View File
@@ -365,17 +365,17 @@ impl CodecParser for H264Parser {
const HIGH_PROFILES: [u8; 14] = [ const HIGH_PROFILES: [u8; 14] = [
100, 110, 122, 144, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135, 100, 110, 122, 144, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135,
]; ];
if HIGH_PROFILES.contains(&profile_idc) { if HIGH_PROFILES.contains(&profile_idc)
if let Some((chroma_fmt, depth_luma, depth_chroma)) = parse_sps_high_profile_ext(sps) { && let Some((chroma_fmt, depth_luma, depth_chroma)) = parse_sps_high_profile_ext(sps)
// byte 0: 111111xx — reserved(6) + chroma_format_idc(2) {
record.push(0xFC | (chroma_fmt & 0x03)); // byte 0: 111111xx — reserved(6) + chroma_format_idc(2)
// byte 1: 11111xxx — reserved(5) + bit_depth_luma_minus8(3) record.push(0xFC | (chroma_fmt & 0x03));
record.push(0xF8 | (depth_luma & 0x07)); // byte 1: 11111xxx — reserved(5) + bit_depth_luma_minus8(3)
// byte 2: 11111xxx — reserved(5) + bit_depth_chroma_minus8(3) record.push(0xF8 | (depth_luma & 0x07));
record.push(0xF8 | (depth_chroma & 0x07)); // byte 2: 11111xxx — reserved(5) + bit_depth_chroma_minus8(3)
// byte 3: num_of_sequence_parameter_set_ext (0 = none) record.push(0xF8 | (depth_chroma & 0x07));
record.push(0x00); // byte 3: num_of_sequence_parameter_set_ext (0 = none)
} record.push(0x00);
} }
Some(record) Some(record)
+18 -19
View File
@@ -307,11 +307,10 @@ impl HevcParser {
}; };
let rbsp = strip_emulation_prevention(raw); let rbsp = strip_emulation_prevention(raw);
let mut i = 0usize; let mut i = 0usize;
loop { // payloadType: sum of 0xFF run + final byte. Exhausting the RBSP ends the
// payloadType: sum of 0xFF run + final byte. // walk; the remaining `let ... else break` arms below handle a TRUNCATED
let Some(payload_type) = read_sei_ff_value(&rbsp, &mut i) else { // message, which is a different condition from a clean end.
break; while let Some(payload_type) = read_sei_ff_value(&rbsp, &mut i) {
};
// payloadSize: same ff-extension coding. // payloadSize: same ff-extension coding.
let Some(payload_size) = read_sei_ff_value(&rbsp, &mut i) else { let Some(payload_size) = read_sei_ff_value(&rbsp, &mut i) else {
break; break;
@@ -504,11 +503,11 @@ impl CodecParser for HevcParser {
// 33-bit counter wrapped: add another period and re-check, rather // 33-bit counter wrapped: add another period and re-check, rather
// than treat the wrap as a backward clip reset. // than treat the wrap as a backward clip reset.
let mut unwrapped = raw_pts + self.pts_wrap_offset; let mut unwrapped = raw_pts + self.pts_wrap_offset;
if let Some(high) = self.high_pts { if let Some(high) = self.high_pts
if high - unwrapped > PTS_WRAP_PERIOD / 2 { && high - unwrapped > PTS_WRAP_PERIOD / 2
self.pts_wrap_offset += PTS_WRAP_PERIOD; {
unwrapped += PTS_WRAP_PERIOD; self.pts_wrap_offset += PTS_WRAP_PERIOD;
} unwrapped += PTS_WRAP_PERIOD;
} }
match self.high_pts { match self.high_pts {
Some(high) if unwrapped < high - BACKSTEP_TICKS => { Some(high) if unwrapped < high - BACKSTEP_TICKS => {
@@ -564,18 +563,18 @@ impl CodecParser for HevcParser {
// `num_extra_slice_header_bits` — and thus the bit offset to // `num_extra_slice_header_bits` — and thus the bit offset to
// `slice_type` — is EXACT. With no PPS we decline rather than // `slice_type` — is EXACT. With no PPS we decline rather than
// guess, leaving coding `None` (honestly absent). // guess, leaving coding `None` (honestly absent).
if coding_type.is_none() && nal_type <= NAL_VCL_MAX { if coding_type.is_none()
if let Some(num_extra) = self && nal_type <= NAL_VCL_MAX
&& let Some(num_extra) = self
.cur_pps .cur_pps
.as_deref() .as_deref()
.and_then(hevc_num_extra_slice_header_bits) .and_then(hevc_num_extra_slice_header_bits)
{ {
coding_type = hevc_first_slice_coding_type( coding_type = hevc_first_slice_coding_type(
&data[nal_start..end], &data[nal_start..end],
nal_type, nal_type,
num_extra, num_extra,
); );
}
} }
match nal_type { match nal_type {
+4 -4
View File
@@ -196,10 +196,10 @@ impl Mpeg2Parser {
if let Some(h) = extract_seq_header(&data) { if let Some(h) = extract_seq_header(&data) {
self.progressive_sequence = parse_progressive_sequence(&h); self.progressive_sequence = parse_progressive_sequence(&h);
self.seq_header = Some(h); self.seq_header = Some(h);
if let Some((num, den)) = self.frame_rate() { if let Some((num, den)) = self.frame_rate()
if num > 0 { && num > 0
self.frame_duration_ns = 1_000_000_000i64 * den as i64 / num as i64; {
} self.frame_duration_ns = 1_000_000_000i64 * den as i64 / num as i64;
} }
} }
// A GOP header (0xB8) or a fresh sequence header (0xB3) starts a new GOP, // A GOP header (0xB8) or a fresh sequence header (0xB3) starts a new GOP,
+6 -6
View File
@@ -168,12 +168,12 @@ impl SparsePtsReorder {
// (assumes both anchors sit at a similar relative display slot), // (assumes both anchors sit at a similar relative display slot),
// but each GOP re-locks its own origin, so the estimate only sets // but each GOP re-locks its own origin, so the estimate only sets
// intra-GOP spacing. // intra-GOP spacing.
if self.dur_ns == 0 { if self.dur_ns == 0
if let (Some((p_held, _)), Some((p_next, _))) = (held.anchor, gop.anchor) { && let (Some((p_held, _)), Some((p_next, _))) = (held.anchor, gop.anchor)
let span = p_next - p_held; {
if span > 0 && held.count > 0 { let span = p_next - p_held;
self.dur_ns = (span / held.count).max(1); if span > 0 && held.count > 0 {
} self.dur_ns = (span / held.count).max(1);
} }
} }
out = self.emit_gop(held); out = self.emit_gop(held);
+43 -43
View File
@@ -344,49 +344,49 @@ impl CodecParser for TrueHdParser {
// mid-AU would snap that AU's PTS backward/forward and break the // mid-AU would snap that AU's PTS backward/forward and break the
// monotonic +AU_DURATION_NS cadence (A/V drift). Once the buffer is empty // monotonic +AU_DURATION_NS cadence (A/V drift). Once the buffer is empty
// the next PES legitimately begins a new AU and seeds the base. // the next PES legitimately begins a new AU and seeds the base.
if self.buf.is_empty() { if self.buf.is_empty()
if let Some(pts) = pes.pts { && let Some(pts) = pes.pts
// Resync to the authoritative PES PTS. TrueHD AUs are a fixed {
// sample count (40 @ 48 kHz), so the per-AU `+AU_DURATION_NS` // Resync to the authoritative PES PTS. TrueHD AUs are a fixed
// cadence is sample-accurate — more so than the disc's per-PES // sample count (40 @ 48 kHz), so the per-AU `+AU_DURATION_NS`
// PTS, which carries the source muxer's own rounding jitter. // cadence is sample-accurate — more so than the disc's per-PES
// // PTS, which carries the source muxer's own rounding jitter.
// Two distinct backward steps must be handled OPPOSITELY: //
// // Two distinct backward steps must be handled OPPOSITELY:
// 1. Small backward jitter (sub-second PES rounding): when the //
// buffer empties exactly on a PES boundary and that PES's PTS // 1. Small backward jitter (sub-second PES rounding): when the
// lands a few ticks *below* the running cadence, an // buffer empties exactly on a PES boundary and that PES's PTS
// unconditional reset would set the next AU's timestamp below // lands a few ticks *below* the running cadence, an
// the AU just emitted, producing non-monotonic block // unconditional reset would set the next AU's timestamp below
// timestamps a muxer rejects. CLAMP to the running position so // the AU just emitted, producing non-monotonic block
// output stays strictly monotonic. // timestamps a muxer rejects. CLAMP to the running position so
// // output stays strictly monotonic.
// 2. Large backward step (> DISCONTINUITY_BACKSTEP_NS): this is a //
// clip-boundary PTS reset — the title's clips are read as one // 2. Large backward step (> DISCONTINUITY_BACKSTEP_NS): this is a
// concatenated stream and a non-seamless boundary resets the // clip-boundary PTS reset — the title's clips are read as one
// source PES PTS near zero. This is NOT jitter and must NOT be // concatenated stream and a non-seamless boundary resets the
// clamped: clamping strands the audio at the previous clip's // source PES PTS near zero. This is NOT jitter and must NOT be
// tail cadence, so when `TimelineContinuity` later bumps the // clamped: clamping strands the audio at the previous clip's
// global offset for the new epoch (driven by the video // tail cadence, so when `TimelineContinuity` later bumps the
// back-jump) the stranded-high audio PTS is flung ~a whole // global offset for the new epoch (driven by the video
// clip past the frontier, producing the non-monotonic // back-jump) the stranded-high audio PTS is flung ~a whole
// audio-DTS band on multi-clip titles (Dune: Part Two, Top // clip past the frontier, producing the non-monotonic
// Gun). ADOPT the raw reset so the per-track raw PTS that // audio-DTS band on multi-clip titles (Dune: Part Two, Top
// reaches `TimelineContinuity` carries the true boundary, and // Gun). ADOPT the raw reset so the per-track raw PTS that
// the corrector rebases it exactly as it already does for the // reaches `TimelineContinuity` carries the true boundary, and
// DTS / AC-3 parsers (which never clamp). Same threshold the // the corrector rebases it exactly as it already does for the
// timeline corrector uses to classify a discontinuity. // DTS / AC-3 parsers (which never clamp). Same threshold the
// // timeline corrector uses to classify a discontinuity.
// A genuine forward gap/discontinuity is always adopted by the //
// `.max()`. // A genuine forward gap/discontinuity is always adopted by the
let new = pts_to_ns(pts); // `.max()`.
if new < self.next_pts_ns - DISCONTINUITY_BACKSTEP_NS { let new = pts_to_ns(pts);
// Clip-boundary reset: take the raw PTS, restart the cadence. if new < self.next_pts_ns - DISCONTINUITY_BACKSTEP_NS {
self.next_pts_ns = new; // Clip-boundary reset: take the raw PTS, restart the cadence.
} else { self.next_pts_ns = new;
// Within-clip jitter (or forward progression): stay monotonic. } else {
self.next_pts_ns = self.next_pts_ns.max(new); // Within-clip jitter (or forward progression): stay monotonic.
} self.next_pts_ns = self.next_pts_ns.max(new);
} }
} }
+4 -6
View File
@@ -216,12 +216,11 @@ impl CodecParser for Vc1Parser {
let end = find_next_sc(data, i + 4).unwrap_or(data.len()); let end = find_next_sc(data, i + 4).unwrap_or(data.len());
let sh = &data[i..end]; let sh = &data[i..end];
// Try to parse resolution from advanced profile sequence header // Try to parse resolution from advanced profile sequence header
if self.seq_header.is_none() { if self.seq_header.is_none()
if let Some((w, h)) = parse_vc1_resolution(sh) { && let Some((w, h)) = parse_vc1_resolution(sh) {
self.width = w; self.width = w;
self.height = h; self.height = h;
} }
}
// Collect into a scratch Vec so handle_header can // Collect into a scratch Vec so handle_header can
// append; we discard the Vec and only keep the flag. // append; we discard the Vec and only keep the flag.
let mut scratch = Vec::new(); let mut scratch = Vec::new();
@@ -250,12 +249,11 @@ impl CodecParser for Vc1Parser {
} }
has_entry_point = true; has_entry_point = true;
} }
SC_FRAME => { SC_FRAME
// Frame data starts at this start code // Frame data starts at this start code
if frame_start.is_none() { if frame_start.is_none() => {
frame_start = Some(i); frame_start = Some(i);
} }
}
_ => {} _ => {}
} }
i += 4; i += 4;
+43 -44
View File
@@ -512,16 +512,16 @@ impl DiscStream {
// priority, mirroring the multipass sweep's transport-failure rule // priority, mirroring the multipass sweep's transport-failure rule
// in `read_error::handle_read_error`. The CLI/UX surfaces this so the // in `read_error::handle_read_error`. The CLI/UX surfaces this so the
// user power-cycles the drive (or switches to multipass recovery). // user power-cycles the drive (or switches to multipass recovery).
if let Some(e) = res.as_ref().err() { if let Some(e) = res.as_ref().err()
if e.is_scsi_transport_failure() { && e.is_scsi_transport_failure()
let (status, sense) = extract_scsi_context(e); {
return Err(crate::error::Error::DiscRead { let (status, sense) = extract_scsi_context(e);
sector: lba as u64, return Err(crate::error::Error::DiscRead {
status: Some(status), sector: lba as u64,
sense, status: Some(status),
} sense,
.into());
} }
.into());
} }
if (sectors as u32) <= align { if (sectors as u32) <= align {
@@ -571,16 +571,16 @@ impl DiscStream {
// a dead bridge as a skippable unit and marching the whole disc // a dead bridge as a skippable unit and marching the whole disc
// at one bridge-recovery per probe (hard rule #2, "runs forever, // at one bridge-recovery per probe (hard rule #2, "runs forever,
// no MKV"). Re-check `rec` and abort, mirroring line 442. // no MKV"). Re-check `rec` and abort, mirroring line 442.
if let Some(e) = rec.as_ref().err() { if let Some(e) = rec.as_ref().err()
if e.is_scsi_transport_failure() { && e.is_scsi_transport_failure()
let (status, sense) = extract_scsi_context(e); {
return Err(crate::error::Error::DiscRead { let (status, sense) = extract_scsi_context(e);
sector: lba as u64, return Err(crate::error::Error::DiscRead {
status: Some(status), sector: lba as u64,
sense, status: Some(status),
} sense,
.into());
} }
.into());
} }
// Recovery read also failed. Skip the WHOLE failed unit or bail. // Recovery read also failed. Skip the WHOLE failed unit or bail.
@@ -725,29 +725,27 @@ impl crate::pes::Stream for DiscStream {
for pes in &demuxer.flush() { for pes in &demuxer.flush() {
if let Some((_, track)) = if let Some((_, track)) =
self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid)
{ && let Some((_, parser)) =
if let Some((_, parser)) =
self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid)
{ {
let resync = &mut self.resync; let resync = &mut self.resync;
let is_video = &self.is_video; let is_video = &self.is_video;
let pending = &mut self.pending_frames; let pending = &mut self.pending_frames;
for frame in parser.parse(pes) { for frame in parser.parse(pes) {
// Same B1 gate — a concealed gap can leave a // Same B1 gate — a concealed gap can leave a
// post-gap frame in the demuxer's final flush. // post-gap frame in the demuxer's final flush.
let emit = match resync.get_mut(*track) { let emit = match resync.get_mut(*track) {
Some(gate) => gate.admit( Some(gate) => gate.admit(
is_video.get(*track).copied().unwrap_or(false), is_video.get(*track).copied().unwrap_or(false),
frame.discontinuity, frame.discontinuity,
frame.keyframe, frame.keyframe,
), ),
None => true, None => true,
}; };
if emit { if emit {
pending.push_back(crate::pes::PesFrame::from_codec_frame( pending.push_back(crate::pes::PesFrame::from_codec_frame(
*track, frame, *track, frame,
)); ));
}
} }
} }
} }
@@ -1021,10 +1019,11 @@ impl crate::pes::Stream for DiscStream {
return true; return true;
} }
for (idx, s) in self.title.streams.iter().enumerate() { for (idx, s) in self.title.streams.iter().enumerate() {
if let crate::disc::Stream::Video(v) = s { if let crate::disc::Stream::Video(v) = s
if !v.secondary && self.codec_private(idx).is_none() { && !v.secondary
return false; && self.codec_private(idx).is_none()
} {
return false;
} }
} }
true true
+1 -1
View File
@@ -436,7 +436,7 @@ impl<W: Write> M2tsMux<W> {
} }
fn maybe_emit_psi(&mut self) -> io::Result<()> { fn maybe_emit_psi(&mut self) -> io::Result<()> {
if self.packets_written == 0 || self.packets_written % PSI_INTERVAL_PACKETS == 0 { if self.packets_written == 0 || self.packets_written.is_multiple_of(PSI_INTERVAL_PACKETS) {
self.emit_pat()?; self.emit_pat()?;
self.emit_pmt()?; self.emit_pmt()?;
} }
+32 -33
View File
@@ -1583,10 +1583,11 @@ impl<W: Write + Seek> MkvMuxer<W> {
// track: the ReferenceBlock above is emitted for any video track, so a // track: the ReferenceBlock above is emitted for any video track, so a
// single global slot produced cross-track references on a multi-video-track // single global slot produced cross-track references on a multi-video-track
// title (MVC base + secondary view, or a disc with two angles). // title (MVC base + secondary view, or a disc with two angles).
if keyframe && is_video { if keyframe
if let Some(slot) = self.last_video_keyframe_ticks.get_mut(track_idx) { && is_video
*slot = Some(pts_ticks); && let Some(slot) = self.last_video_keyframe_ticks.get_mut(track_idx)
} {
*slot = Some(pts_ticks);
} }
self.frame_count += 1; self.frame_count += 1;
@@ -1600,29 +1601,29 @@ impl<W: Write + Seek> MkvMuxer<W> {
// (it claims 5.1 on a 2.0 stream); the bitstream acmod is authoritative. // (it claims 5.1 on a 2.0 stream); the bitstream acmod is authoritative.
// Only the first frame triggers it; the byte width is unchanged so the // Only the first frame triggers it; the byte width is unchanged so the
// patch is a single-byte in-place rewrite (then restore position). // patch is a single-byte in-place rewrite (then restore position).
if let Some(fixup) = self.ac3_channel_fixups.get_mut(&track_idx) { if let Some(fixup) = self.ac3_channel_fixups.get_mut(&track_idx)
if !fixup.corrected { && !fixup.corrected
match super::codec::ac3::acmod_channels(data) { {
Some(actual) if actual > 0 => { match super::codec::ac3::acmod_channels(data) {
if actual != fixup.claimed { Some(actual) if actual > 0 => {
tracing::warn!( if actual != fixup.claimed {
target: "mux", tracing::warn!(
"AC-3 track {track_idx}: IFO claimed {} channels but bitstream acmod says {}; trusting the bitstream (possible wrong-stream selection)", target: "mux",
fixup.claimed, "AC-3 track {track_idx}: IFO claimed {} channels but bitstream acmod says {}; trusting the bitstream (possible wrong-stream selection)",
actual, fixup.claimed,
); actual,
let here = self.writer.stream_position()?; );
self.writer let here = self.writer.stream_position()?;
.seek(std::io::SeekFrom::Start(fixup.value_offset))?; self.writer
self.writer.write_all(&[actual])?; .seek(std::io::SeekFrom::Start(fixup.value_offset))?;
self.writer.seek(std::io::SeekFrom::Start(here))?; self.writer.write_all(&[actual])?;
} self.writer.seek(std::io::SeekFrom::Start(here))?;
fixup.corrected = true;
} }
// Frame too short to carry the BSI bits — keep the passed fixup.corrected = true;
// (IFO) value and try again on the next frame.
_ => {}
} }
// Frame too short to carry the BSI bits — keep the passed
// (IFO) value and try again on the next frame.
_ => {}
} }
} }
@@ -1767,14 +1768,12 @@ impl<W: Write + Seek> MkvMuxer<W> {
// with a 1-byte size VINT covering the remaining 19 bytes occupies // with a 1-byte size VINT covering the remaining 19 bytes occupies
// exactly 1 + 1 + 19 = 21 bytes, overwriting the entry in place without // exactly 1 + 1 + 19 = 21 bytes, overwriting the entry in place without
// shifting any following element. // shifting any following element.
if !have_cues { if !have_cues && let Some(entry_pos) = self.cues_seek_entry_pos {
if let Some(entry_pos) = self.cues_seek_entry_pos { self.writer.seek(std::io::SeekFrom::Start(entry_pos))?;
self.writer.seek(std::io::SeekFrom::Start(entry_pos))?; ebml::write_id(&mut self.writer, ebml::VOID)?;
ebml::write_id(&mut self.writer, ebml::VOID)?; // 19 = 21-byte entry minus the Void ID (1) and size (1) bytes.
// 19 = 21-byte entry minus the Void ID (1) and size (1) bytes. ebml::write_size(&mut self.writer, 19)?;
ebml::write_size(&mut self.writer, 19)?; self.writer.write_all(&[0u8; 19])?;
self.writer.write_all(&[0u8; 19])?;
}
} }
self.writer.seek(std::io::SeekFrom::End(0))?; self.writer.seek(std::io::SeekFrom::End(0))?;
+5 -5
View File
@@ -254,11 +254,11 @@ impl MvcMerge {
.front() .front()
.map(|pb| pb.additional.is_some()) .map(|pb| pb.additional.is_some())
.unwrap_or(false); .unwrap_or(false);
if front_ready || self.pending_base.len() > MVC_PAIR_WINDOW { if (front_ready || self.pending_base.len() > MVC_PAIR_WINDOW)
if let Some(pb) = self.pending_base.pop_front() { && let Some(pb) = self.pending_base.pop_front()
out.push((pb.frame, pb.additional)); {
continue; out.push((pb.frame, pb.additional));
} continue;
} }
break; break;
} }
+3 -5
View File
@@ -159,11 +159,9 @@ fn parse_eac3(f: &[u8]) -> Option<DolbyConfig> {
// samples at sample_rate. rate = bytes·8·sr / samples / 1000. // samples at sample_rate. rate = bytes·8·sr / samples / 1000.
let frame_bytes = (frmsiz as u64 + 1) * 2; let frame_bytes = (frmsiz as u64 + 1) * 2;
let samples = blocks as u64 * 256; let samples = blocks as u64 * 256;
let data_rate_kbps = if samples > 0 { let data_rate_kbps = (frame_bytes * 8 * sample_rate as u64)
((frame_bytes * 8 * sample_rate as u64) / samples / 1000) as u16 .checked_div(samples)
} else { .map_or(0, |r| (r / 1000) as u16);
0
};
Some(DolbyConfig { Some(DolbyConfig {
fscod, fscod,
+11 -8
View File
@@ -285,11 +285,13 @@ impl<W: Write + Seek> Mp4Sink<W> {
let mut tracks = Vec::new(); let mut tracks = Vec::new();
let mut route = vec![None; title.streams.len()]; let mut route = vec![None; title.streams.len()];
let mut next_id = 1u32;
let mut video_codec = Codec::Hevc; let mut video_codec = Codec::Hevc;
for &i in &report.included { // Track ids are 1-based and assigned in inclusion order. `moov`'s
let track_id = next_id; // next_track_id is NOT derived from this counter — it is max(track_id) + 1
next_id += 1; // computed after the sample-less retain, since ids are handed out here
// before any track is dropped.
for (n, &i) in report.included.iter().enumerate() {
let track_id = n as u32 + 1;
route[i] = Some(tracks.len()); route[i] = Some(tracks.len());
match &title.streams[i] { match &title.streams[i] {
DiscStream::Video(v) => { DiscStream::Video(v) => {
@@ -440,10 +442,11 @@ impl<W: Write + Seek + Send> Stream for Mp4Sink<W> {
// cost us the frame. Dropping leading frames here lost audio silently, and // cost us the frame. Dropping leading frames here lost audio silently, and
// a track whose frames never parsed vanished from the output entirely with // a track whose frames never parsed vanished from the output entirely with
// no report; finish() now decides that case loudly instead. // no report; finish() now decides that case loudly instead.
if self.tracks[slot].media == Media::Audio && self.tracks[slot].audio_entry.is_none() { if self.tracks[slot].media == Media::Audio
if let Some(entry) = audio::dolby_sample_entry(self.tracks[slot].codec, &frame.data) { && self.tracks[slot].audio_entry.is_none()
self.tracks[slot].audio_entry = Some(entry); && let Some(entry) = audio::dolby_sample_entry(self.tracks[slot].codec, &frame.data)
} {
self.tracks[slot].audio_entry = Some(entry);
} }
let pts_ns = frame.pts; let pts_ns = frame.pts;
let offset = self.mdat_start + 16 + self.mdat_payload; let offset = self.mdat_start + 16 + self.mdat_payload;
+5 -4
View File
@@ -420,10 +420,11 @@ impl Stream for PipelinedPesStream {
return true; return true;
} }
for (idx, s) in self.title.streams.iter().enumerate() { for (idx, s) in self.title.streams.iter().enumerate() {
if let crate::disc::Stream::Video(v) = s { if let crate::disc::Stream::Video(v) = s
if !v.secondary && self.codec_private(idx).is_none() { && !v.secondary
return false; && self.codec_private(idx).is_none()
} {
return false;
} }
} }
true true
+15 -16
View File
@@ -193,10 +193,10 @@ pub fn parse_url(url: &str) -> StreamUrl {
return StreamUrl::Null; return StreamUrl::Null;
} }
} }
if let Some(rest) = url.strip_prefix("stdio://") { if let Some(rest) = url.strip_prefix("stdio://")
if rest.is_empty() { && rest.is_empty()
return StreamUrl::Stdio; {
} return StreamUrl::Stdio;
} }
if let Some(rest) = url.strip_prefix("iso://") { if let Some(rest) = url.strip_prefix("iso://") {
return StreamUrl::Iso { return StreamUrl::Iso {
@@ -1646,20 +1646,19 @@ pub(crate) fn resolve_mux_key_map_cached(
_ => Vec::new(), _ => Vec::new(),
}; };
let mut idx = pick(&samples, &pool); let mut idx = pick(&samples, &pool);
if idx.is_none() { if idx.is_none()
if let Some(f) = fetch { && let Some(f) = fetch
if !samples.is_empty() { && !samples.is_empty()
let fresh = f.unit_keys(&samples); {
if let crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } = keys { let fresh = f.unit_keys(&samples);
for k in fresh { if let crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } = keys {
if !unit_keys.iter().any(|(_, h)| *h == k) { for k in fresh {
let i = unit_keys.len() as u32; if !unit_keys.iter().any(|(_, h)| *h == k) {
unit_keys.push((i, k)); let i = unit_keys.len() as u32;
} unit_keys.push((i, k));
}
idx = pick(&samples, unit_keys);
} }
} }
idx = pick(&samples, unit_keys);
} }
} }
// `sample_units` draws REAL content (not authored-bad units), so a sample // `sample_units` draws REAL content (not authored-bad units), so a sample
+6 -6
View File
@@ -53,12 +53,12 @@ impl StdioStream {
/// zero-frame output stream still emits the magic + metadata header, /// zero-frame output stream still emits the magic + metadata header,
/// keeping the wire protocol symmetric with the read side's read_header(). /// keeping the wire protocol symmetric with the read side's read_header().
fn ensure_header_written(&mut self) -> io::Result<()> { fn ensure_header_written(&mut self) -> io::Result<()> {
if let Some(w) = &mut self.writer { if let Some(w) = &mut self.writer
if !self.header_written { && !self.header_written
let m = meta::M2tsMeta::from_title(&self.disc_title); {
meta::write_header(w, &m)?; let m = meta::M2tsMeta::from_title(&self.disc_title);
self.header_written = true; meta::write_header(w, &m)?;
} self.header_written = true;
} }
Ok(()) Ok(())
} }
+8 -8
View File
@@ -140,14 +140,14 @@ impl TimelineContinuity {
// overflow: a panic out of the public `Stream::write` path in an // overflow: a panic out of the public `Stream::write` path in an
// overflow-checked build, and in release a wrap to the opposite sign // overflow-checked build, and in release a wrap to the opposite sign
// that fires the straggler clamp on essentially every passive frame. // that fires the straggler clamp on essentially every passive frame.
if let Some(high) = self.high_ns { if let Some(high) = self.high_ns
if mapped > high.saturating_add(DISCONTINUITY_BACKSTEP_NS) { && mapped > high.saturating_add(DISCONTINUITY_BACKSTEP_NS)
let prev_mapped = raw_pts_ns.saturating_add(self.prev_offset_ns); {
if prev_mapped <= high let prev_mapped = raw_pts_ns.saturating_add(self.prev_offset_ns);
&& prev_mapped >= high.saturating_sub(DISCONTINUITY_BACKSTEP_NS) if prev_mapped <= high
{ && prev_mapped >= high.saturating_sub(DISCONTINUITY_BACKSTEP_NS)
return prev_mapped; {
} return prev_mapped;
} }
} }
return mapped; return mapped;
+1 -1
View File
@@ -233,7 +233,7 @@ impl Heartbeat {
/// iterations. Otherwise identical to [`tick`](Heartbeat::tick). /// iterations. Otherwise identical to [`tick`](Heartbeat::tick).
pub fn tick_cpu(&mut self, pos: u64, total: u64) -> bool { pub fn tick_cpu(&mut self, pos: u64, total: u64) -> bool {
self.cpu_counter = self.cpu_counter.wrapping_add(1); self.cpu_counter = self.cpu_counter.wrapping_add(1);
if self.cpu_counter % 256 != 0 { if !self.cpu_counter.is_multiple_of(256) {
return false; return false;
} }
self.tick(pos, total) self.tick(pos, total)
+3 -1
View File
@@ -227,7 +227,9 @@ impl PrefetchedSectorSource {
// way to hand the decrypt step an aligned chunk — // way to hand the decrypt step an aligned chunk —
// surface a typed error instead of emitting // surface a typed error instead of emitting
// still-encrypted bytes. // still-encrypted bytes.
if remaining % unit_align as u32 != 0 && remaining < unit_align as u32 { if !remaining.is_multiple_of(unit_align as u32)
&& remaining < unit_align as u32
{
let _ = tx.send(Err(crate::error::Error::ExtentNotUnitAligned.into())); let _ = tx.send(Err(crate::error::Error::ExtentNotUnitAligned.into()));
return; return;
} }
+4 -4
View File
@@ -1264,10 +1264,10 @@ fn parse_dstring(data: &[u8]) -> String {
for i in (0..chars.len()).step_by(2) { for i in (0..chars.len()).step_by(2) {
if i + 1 < chars.len() { if i + 1 < chars.len() {
let c = ((chars[i] as u16) << 8) | chars[i + 1] as u16; let c = ((chars[i] as u16) << 8) | chars[i + 1] as u16;
if c != 0 { if c != 0
if let Some(ch) = char::from_u32(c as u32) { && let Some(ch) = char::from_u32(c as u32)
s.push(ch); {
} s.push(ch);
} }
} }
} }