diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 593381f..06835ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@1.87.0 + - uses: dtolnay/rust-toolchain@1.97.0 with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@1.87.0 + - uses: dtolnay/rust-toolchain@1.97.0 - uses: Swatinem/rust-cache@v2 - run: cargo test --tests @@ -33,7 +33,7 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@1.87.0 + - uses: dtolnay/rust-toolchain@1.97.0 - uses: Swatinem/rust-cache@v2 - run: cargo check @@ -41,7 +41,7 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@1.87.0 + - uses: dtolnay/rust-toolchain@1.97.0 - uses: Swatinem/rust-cache@v2 # Build the tests (not just `cargo check`): catches errors in test # code and forces full codegen of the Windows-only SPTI transport diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a86a2b4..af2d7e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: # 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 - # 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 # the instant this tag exists — so this test job and the crates.io publish # below must NOT sit on their critical path. @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@1.87.0 + - uses: dtolnay/rust-toolchain@1.97.0 - uses: Swatinem/rust-cache@v2 # libfreemkv is a library — Cargo.lock isn't tracked, so --locked # would always fail (no lockfile to lock against on a fresh runner). diff --git a/Cargo.toml b/Cargo.toml index 68e77cc..e291413 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "libfreemkv" version = "1.6.0" edition = "2024" -rust-version = "1.87" +rust-version = "1.97" license = "MIT" description = "Open source raw disc access library for optical drives" repository = "https://github.com/freemkv/libfreemkv" diff --git a/build.rs b/build.rs index 6398ddc..0a71ae7 100644 --- a/build.rs +++ b/build.rs @@ -77,10 +77,10 @@ fn emit_git_suffix() { // Re-run when HEAD (or the branch it points at) moves so the stamp stays // current without a clean rebuild. println!("cargo:rerun-if-changed=.git/HEAD"); - if let Ok(head) = std::fs::read_to_string(".git/HEAD") { - if let Some(ref_path) = head.strip_prefix("ref: ") { - println!("cargo:rerun-if-changed=.git/{}", ref_path.trim()); - } + if let Ok(head) = std::fs::read_to_string(".git/HEAD") + && let Some(ref_path) = head.strip_prefix("ref: ") + { + println!("cargo:rerun-if-changed=.git/{}", ref_path.trim()); } } diff --git a/src/aacs/content.rs b/src/aacs/content.rs index ca447f7..5727e47 100644 --- a/src/aacs/content.rs +++ b/src/aacs/content.rs @@ -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 /// alignment (e.g. `lba == unit_base - 1` would falsely read as aligned). 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; diff --git a/src/aacs/crypto.rs b/src/aacs/crypto.rs index e709679..c588044 100644 --- a/src/aacs/crypto.rs +++ b/src/aacs/crypto.rs @@ -49,7 +49,7 @@ pub(crate) fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] { /// expansions. pub(crate) fn aes_cbc_encrypt(key: &[u8; 16], data: &mut [u8]) { debug_assert!( - data.len() % 16 == 0, + data.len().is_multiple_of(16), "aes_cbc_encrypt requires a block-aligned slice" ); 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]) { debug_assert!( - data.len() % 16 == 0, + data.len().is_multiple_of(16), "aes_cbc_decrypt requires a block-aligned slice" ); let cipher = Aes128::new(GenericArray::from_slice(key)); diff --git a/src/clpi.rs b/src/clpi.rs index 1672e85..a5c3235 100644 --- a/src/clpi.rs +++ b/src/clpi.rs @@ -328,10 +328,8 @@ fn parse_program_info(data: &[u8]) -> Vec { } } // PG, IG: coding_type + 3-byte language [+ char_code for PG] - c::PG | c::IG => { - if sci.len() >= 4 { - language = String::from_utf8_lossy(&sci[1..4]).to_string(); - } + c::PG | c::IG if sci.len() >= 4 => { + language = String::from_utf8_lossy(&sci[1..4]).to_string(); } _ => {} } diff --git a/src/css/mod.rs b/src/css/mod.rs index 6f3ddc1..ab8e928 100644 --- a/src/css/mod.rs +++ b/src/css/mod.rs @@ -249,10 +249,10 @@ fn crack_key_scan( while i < ext.sector_count && tried < max_tries { // Cooperative cancellation — poll once per batch, the same cadence // sweep/patch use, so a Stop / watchdog can interrupt the scan. - if let Some(h) = halt { - if h.is_cancelled() { - break 'outer; - } + if let Some(h) = halt + && h.is_cancelled() + { + break 'outer; } // Liveness beacon: a long scan over a damaged disc stays visible. // 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); } lfsr::descramble_sector(title_key, chunk); - if let Some(crib) = crib { - if chunk[0x80..0x80 + 10] != crib[..] { - // Cached key is stale for this region — restore the ciphertext and - // crack this sector's own key. - chunk.copy_from_slice(&original); - if let Some(fresh) = stevenson::crack_title_key(chunk) { - *title_key = fresh; - } - lfsr::descramble_sector(title_key, chunk); + if let Some(crib) = crib + && chunk[0x80..0x80 + 10] != crib[..] + { + // Cached key is stale for this region — restore the ciphertext and + // crack this sector's own key. + chunk.copy_from_slice(&original); + if let Some(fresh) = stevenson::crack_title_key(chunk) { + *title_key = fresh; } + lfsr::descramble_sector(title_key, chunk); } } } diff --git a/src/decrypt.rs b/src/decrypt.rs index e849bc4..10ff1b5 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -332,11 +332,11 @@ impl AacsKeyMap { if sectors == 0 { return; } - if let Some(last) = plan.last_mut() { - if last.start_lba.saturating_add(last.sector_count) == lba { - last.sector_count += sectors; - return; - } + if let Some(last) = plan.last_mut() + && last.start_lba.saturating_add(last.sector_count) == lba + { + last.sector_count += sectors; + return; } plan.push(crate::disc::Extent { start_lba: lba, diff --git a/src/disc/bluray.rs b/src/disc/bluray.rs index 55132c2..d725463 100644 --- a/src/disc/bluray.rs +++ b/src/disc/bluray.rs @@ -29,12 +29,11 @@ impl Disc { for entry in &playlist_dir.entries { if !entry.is_dir && entry.name.to_lowercase().ends_with(".mpls") { let path = format!("/BDMV/PLAYLIST/{}", entry.name); - if let Ok(mpls_data) = udf_fs.read_file(reader, &path) { - if let Some(title) = + if let Ok(mpls_data) = udf_fs.read_file(reader, &path) + && let Some(title) = 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 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(clip_info) = clpi::parse(&clpi_data) { - pkt_count = clip_info.source_packet_count; + if let Ok(clpi_data) = udf_fs.read_file(reader, &clpi_path) + && let Ok(clip_info) = clpi::parse(&clpi_data) + { + pkt_count = clip_info.source_packet_count; - // Mark the clip seen ONLY after its .clpi parses — a transient - // read/parse failure on the first PlayItem referencing a clip - // must not permanently suppress its extents/size for a later - // PlayItem referencing the same clip that succeeds. - let first_ref = seen_clips.insert(play_item.clip_id.clone()); + // Mark the clip seen ONLY after its .clpi parses — a transient + // read/parse failure on the first PlayItem referencing a clip + // must not permanently suppress its extents/size for a later + // PlayItem referencing the same clip that succeeds. + let first_ref = seen_clips.insert(play_item.clip_id.clone()); - // Only fetch/push the physical extents and add to the - // total size the first time this clip_id is seen. - if first_ref { - total_size += pkt_count as u64 * 192; + // Only fetch/push the physical extents and add to the + // total size the first time this clip_id is seen. + if first_ref { + total_size += pkt_count as u64 * 192; - // Get stream file extents from UDF allocation descriptors. - // Dual-layer discs split files across layers — UDF knows the real layout. - // - // The clip's stream file is normally `.m2ts`, but AACS 2.1 - // (FMTS) discs name the main feature `.fmts` and 3D discs - // use `.ssif` (see [`CLIP_STREAM_EXTS`]). A normal `.m2ts` - // clip is unchanged — the fallback only runs when `.m2ts` - // is absent, which is exactly when `file_extents` errors. - // 3D discs interleave the left (base) and right (MVC - // dependent) views in STREAM/SSIF/.ssif — note the - // SSIF/ subdir. Prefer it when present: the SSIF is one - // transport stream carrying BOTH eyes on distinct PIDs, - // so muxing it captures the full 3D. 2D clips fall back to - // the base .m2ts / .fmts as before. - let ssif = format!("/BDMV/STREAM/SSIF/{}.ssif", play_item.clip_id); - let file_exts = match udf_fs.file_extents(reader, &ssif) { - Ok(exts) => { - is_3d = true; - Some(exts) - } - Err(_) => CLIP_STREAM_EXTS.iter().find_map(|ext| { - let path = format!("/BDMV/STREAM/{}.{}", play_item.clip_id, ext); - udf_fs.file_extents(reader, &path).ok() - }), - }; - if let Some(file_exts) = file_exts { - for (lba, sectors) in file_exts { - if sectors > 0 && lba > 0 { - extents.push(Extent { - start_lba: lba, - sector_count: sectors, - }); - } + // Get stream file extents from UDF allocation descriptors. + // Dual-layer discs split files across layers — UDF knows the real layout. + // + // The clip's stream file is normally `.m2ts`, but AACS 2.1 + // (FMTS) discs name the main feature `.fmts` and 3D discs + // use `.ssif` (see [`CLIP_STREAM_EXTS`]). A normal `.m2ts` + // clip is unchanged — the fallback only runs when `.m2ts` + // is absent, which is exactly when `file_extents` errors. + // 3D discs interleave the left (base) and right (MVC + // dependent) views in STREAM/SSIF/.ssif — note the + // SSIF/ subdir. Prefer it when present: the SSIF is one + // transport stream carrying BOTH eyes on distinct PIDs, + // so muxing it captures the full 3D. 2D clips fall back to + // the base .m2ts / .fmts as before. + let ssif = format!("/BDMV/STREAM/SSIF/{}.ssif", play_item.clip_id); + let file_exts = match udf_fs.file_extents(reader, &ssif) { + Ok(exts) => { + is_3d = true; + Some(exts) + } + Err(_) => CLIP_STREAM_EXTS.iter().find_map(|ext| { + let path = format!("/BDMV/STREAM/{}.{}", play_item.clip_id, ext); + udf_fs.file_extents(reader, &path).ok() + }), + }; + if let Some(file_exts) = file_exts { + for (lba, sectors) in file_exts { + if sectors > 0 && lba > 0 { + extents.push(Extent { + start_lba: lba, + sector_count: sectors, + }); } } } @@ -266,23 +265,23 @@ impl Disc { // optional) but over-claims 3D for those frames. Real 3D main-feature // playlists are single-clip or uniformly 3D, so this is not exercised; // per-clip 3D would need per-clip stream sets (a larger change). - if is_3d { - if let Some(base) = streams.iter().find_map(|s| match s { + if is_3d + && let Some(base) = streams.iter().find_map(|s| match s { Stream::Video(v) => Some(v.clone()), _ => None, - }) { - let dep_pid = base.pid.wrapping_add(1); - let have_dep = streams - .iter() - .any(|s| matches!(s, Stream::Video(v) if v.pid == dep_pid)); - if !have_dep { - streams.push(Stream::Video(VideoStream { - pid: dep_pid, - secondary: true, - label: crate::disc::MVC_DEPENDENT_LABEL.to_string(), - ..base - })); - } + }) + { + let dep_pid = base.pid.wrapping_add(1); + let have_dep = streams + .iter() + .any(|s| matches!(s, Stream::Video(v) if v.pid == dep_pid)); + if !have_dep { + streams.push(Stream::Video(VideoStream { + pid: dep_pid, + secondary: true, + label: crate::disc::MVC_DEPENDENT_LABEL.to_string(), + ..base + })); } } diff --git a/src/disc/dvd_audio_probe.rs b/src/disc/dvd_audio_probe.rs index 12b35fa..0670da8 100644 --- a/src/disc/dvd_audio_probe.rs +++ b/src/disc/dvd_audio_probe.rs @@ -104,10 +104,10 @@ fn max_substream_channels(data: &[u8]) -> Option { }; let start = pos + rel; let frame = &data[start..]; - if let Some(ch) = ac3::acmod_channels(frame) { - if ch > 0 { - best = Some(best.map_or(ch, |b| b.max(ch))); - } + if let Some(ch) = ac3::acmod_channels(frame) + && ch > 0 + { + best = Some(best.map_or(ch, |b| b.max(ch))); } // 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. diff --git a/src/disc/extract.rs b/src/disc/extract.rs index 800a7a7..1d35e28 100644 --- a/src/disc/extract.rs +++ b/src/disc/extract.rs @@ -165,13 +165,13 @@ impl Disc { .iter() .map(|p| p.size) .fold(0u64, |a, b| a.saturating_add(b)); - if let Some(available) = available_space(dest) { - if available < required { - return Err(Error::DirInsufficientSpace { - required, - available, - }); - } + if let Some(available) = available_space(dest) + && available < required + { + return Err(Error::DirInsufficientSpace { + required, + available, + }); } // 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_disc = format!("{disc_path}/{}", entry.name); // Collision: two distinct disc paths → same host path. - if let Some(prev) = seen_hosts.insert(child_rel.clone(), child_disc.clone()) { - if prev != child_disc { - return Err(Error::DirNameCollision { - host: child_rel.to_string_lossy().into_owned(), - }); - } + if let Some(prev) = seen_hosts.insert(child_rel.clone(), child_disc.clone()) + && prev != child_disc + { + return Err(Error::DirNameCollision { + host: child_rel.to_string_lossy().into_owned(), + }); } if entry.is_dir { dirs.push(child_rel.clone()); @@ -756,10 +756,11 @@ fn is_windows_reserved(base: &str) -> bool { } let up = base.to_ascii_uppercase(); for prefix in ["COM", "LPT"] { - if let Some(rest) = up.strip_prefix(prefix) { - if rest.len() == 1 && matches!(rest.as_bytes()[0], b'1'..=b'9') { - return true; - } + if let Some(rest) = up.strip_prefix(prefix) + && rest.len() == 1 + && matches!(rest.as_bytes()[0], b'1'..=b'9') + { + return true; } } false diff --git a/src/disc/hddvd.rs b/src/disc/hddvd.rs index 86de2fc..a309432 100644 --- a/src/disc/hddvd.rs +++ b/src/disc/hddvd.rs @@ -305,12 +305,12 @@ fn collect_es( } } PRIVATE_STREAM_1 => { - if let Some(sub) = pkt.sub_stream_id { - if (0xC0..=0xC7).contains(&sub) { - let slot = audio.entry(sub).or_default(); - if slot.len() < EVO_ES_SAMPLE_CAP { - slot.extend_from_slice(&pkt.data); - } + if let Some(sub) = pkt.sub_stream_id + && (0xC0..=0xC7).contains(&sub) + { + let slot = audio.entry(sub).or_default(); + if slot.len() < EVO_ES_SAMPLE_CAP { + slot.extend_from_slice(&pkt.data); } } } diff --git a/src/disc/mod.rs b/src/disc/mod.rs index 2bbe0bc..e3b541d 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -687,15 +687,15 @@ pub fn chapter_at_offset( /// The 1-based chapter + movie-time offset an LBA falls in, or `(None, None)` /// if it isn't inside the title. fn range_chapter(lba: u32, title: &DiscTitle) -> (Option, Option) { - if let Some(byte_offset) = byte_offset_in_title(lba, title) { - if let Some((ch, t)) = chapter_at_offset( + if let Some(byte_offset) = byte_offset_in_title(lba, title) + && let Some((ch, t)) = chapter_at_offset( &title.chapters, byte_offset, title.duration_secs, title.size_bytes, - ) { - return (Some(ch as u32), Some(t)); - } + ) + { + return (Some(ch as u32), Some(t)); } (None, None) } @@ -1726,7 +1726,7 @@ impl Disc { { Some(t) => { 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 } None => Vec::new(), @@ -3049,15 +3049,15 @@ pub fn detect_max_batch_sectors(device_path: &str) -> u16 { if let Some(bname) = block_name { 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(kb) = content.trim().parse::() { - // Convert KB to sectors (1 sector = 2 KB = 2048 bytes) - let sectors = (kb / 2).min(u16::MAX as u32) as u16; - // Align down to 3 (one aligned unit) - let aligned = (sectors / 3) * 3; - if aligned >= MIN_BATCH_SECTORS { - return aligned.min(MAX_BATCH_SECTORS); - } + if let Ok(content) = std::fs::read_to_string(&sysfs_path) + && let Ok(kb) = content.trim().parse::() + { + // Convert KB to sectors (1 sector = 2 KB = 2048 bytes) + let sectors = (kb / 2).min(u16::MAX as u32) as u16; + // Align down to 3 (one aligned unit) + let aligned = (sectors / 3) * 3; + if aligned >= MIN_BATCH_SECTORS { + return aligned.min(MAX_BATCH_SECTORS); } } } diff --git a/src/disc/pgs_forced_probe.rs b/src/disc/pgs_forced_probe.rs index ebb854c..c08213e 100644 --- a/src/disc/pgs_forced_probe.rs +++ b/src/disc/pgs_forced_probe.rs @@ -42,7 +42,7 @@ const CHUNK_SECTORS: u16 = 1023; // The alignment requirement above is enforced, not just described. 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" ); @@ -393,12 +393,11 @@ fn verdicts(evidence: &HashMap, conclusive: bool) -> HashMap /// from the map was never observed and keeps its vendor-derived flag. fn apply_verdicts(title: &mut DiscTitle, verdicts: &HashMap) { for s in &mut title.streams { - if let Stream::Subtitle(sub) = s { - if sub.codec == Codec::Pgs { - if let Some(&forced) = verdicts.get(&sub.pid) { - sub.forced = forced; - } - } + if let Stream::Subtitle(sub) = s + && sub.codec == Codec::Pgs + && let Some(&forced) = verdicts.get(&sub.pid) + { + sub.forced = forced; } } } diff --git a/src/drive/macos.rs b/src/drive/macos.rs index 75eda85..09f5ab1 100644 --- a/src/drive/macos.rs +++ b/src/drive/macos.rs @@ -25,12 +25,11 @@ pub fn find_drives() -> Vec<(String, DriveId)> { let path = std::path::Path::new(&info.path); match crate::scsi::open(path) { Ok(mut transport) => { - if let Ok(id) = DriveId::from_drive(transport.as_mut()) { - if !id.raw_inquiry.is_empty() - && (id.raw_inquiry[0] & 0x1F) == SCSI_PERIPHERAL_TYPE_OPTICAL - { - drives.push((info.path.clone(), id)); - } + if let Ok(id) = DriveId::from_drive(transport.as_mut()) + && !id.raw_inquiry.is_empty() + && (id.raw_inquiry[0] & 0x1F) == SCSI_PERIPHERAL_TYPE_OPTICAL + { + drives.push((info.path.clone(), id)); } } Err(_) => { diff --git a/src/hex.rs b/src/hex.rs index 9b2859a..b5b0dab 100644 --- a/src/hex.rs +++ b/src/hex.rs @@ -20,7 +20,7 @@ pub fn parse_hex_bytes(s: &str) -> Option> { let bytes = body.as_bytes(); // Empty → empty Vec (a legitimately-empty variable-length field); odd length // is malformed. (`parse_hex_fixed` enforces a concrete length separately.) - if bytes.len() % 2 != 0 { + if !bytes.len().is_multiple_of(2) { return None; } let mut out = Vec::with_capacity(bytes.len() / 2); diff --git a/src/io/bounded.rs b/src/io/bounded.rs index 2a74afe..e6a804a 100644 --- a/src/io/bounded.rs +++ b/src/io/bounded.rs @@ -133,10 +133,10 @@ where match rx.recv_timeout(slice) { Ok(v) => return Ok(v), Err(RecvTimeoutError::Timeout) => { - if let Some(h) = halt { - if h.is_cancelled() { - return Err(BoundedError::Halted); - } + if let Some(h) = halt + && h.is_cancelled() + { + return Err(BoundedError::Halted); } if Instant::now() >= deadline { return Err(BoundedError::Timeout); diff --git a/src/io/pipeline.rs b/src/io/pipeline.rs index 1741520..8655b34 100644 --- a/src/io/pipeline.rs +++ b/src/io/pipeline.rs @@ -752,15 +752,15 @@ impl Pipeline { Err(payload) => Err(consumer_panicked(payload)), }; } - if let Some(h) = halt { - if h.is_cancelled() { - return finish_with_grace( - handle, - &state, - Duration::from_secs(FINISH_GRACE_SECS), - Error::Halted, - ); - } + if let Some(h) = halt + && h.is_cancelled() + { + return finish_with_grace( + handle, + &state, + Duration::from_secs(FINISH_GRACE_SECS), + Error::Halted, + ); } if Instant::now() >= deadline { return finish_with_grace( diff --git a/src/labels/bdmt.rs b/src/labels/bdmt.rs index 3d3e633..c3ac3f1 100644 --- a/src/labels/bdmt.rs +++ b/src/labels/bdmt.rs @@ -100,10 +100,10 @@ pub fn parse(reader: &mut dyn SectorSource, udf: &UdfFs) -> Option // Disc-set position is disc-global; first one we successfully // read wins. (All bdmt_*.xml on a given disc carry the same // value in practice.) - if out.disc_number.is_none() { - if let Some(ds) = disc_set { - out.disc_number = Some(ds); - } + if out.disc_number.is_none() + && let Some(ds) = disc_set + { + out.disc_number = Some(ds); } } @@ -184,20 +184,20 @@ fn extract_title(xml_text: &str) -> Option { // xml::text already trims its result, so an empty string after // extraction means a genuinely empty element. for tag in ["name", "title"] { - if let Some(s) = xml::text(xml_text, tag) { - if !s.is_empty() { - return Some(s); - } + if let Some(s) = xml::text(xml_text, tag) + && !s.is_empty() + { + return Some(s); } } // tableOfContents/titleName: search inside the toc block so we // don't accidentally pick a stray from elsewhere. if let Some((s, e)) = xml::find_element(xml_text, "tableOfContents", 0) { let block = &xml_text[s..e]; - if let Some(t) = xml::text(block, "titleName") { - if !t.is_empty() { - return Some(t); - } + if let Some(t) = xml::text(block, "titleName") + && !t.is_empty() + { + return Some(t); } } None diff --git a/src/labels/criterion.rs b/src/labels/criterion.rs index 0a287d2..d699b88 100644 --- a/src/labels/criterion.rs +++ b/src/labels/criterion.rs @@ -36,10 +36,10 @@ pub fn parse(reader: &mut dyn SectorSource, udf: &UdfFs) -> Option // Stream number mapping from playbackconfig.xml let mut stream_map: HashMap = HashMap::new(); - if let Some(pc_data) = super::read_jar_file(reader, udf, "playbackconfig.xml") { - if let Ok(pc_text) = std::str::from_utf8(&pc_data) { - parse_playback_config(pc_text, &mut stream_map); - } + if let Some(pc_data) = super::read_jar_file(reader, udf, "playbackconfig.xml") + && let Ok(pc_text) = std::str::from_utf8(&pc_data) + { + parse_playback_config(pc_text, &mut 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) { if let (Some(stream_id_str), Some(info_id)) = ( xml::text(block, "StreamID"), xml::text(block, "StreamInfo_ID"), - ) { - if let Ok(stream_num) = stream_id_str.parse::() { - // Stream numbers are 1-based per the apply_labels - // contract; a mapped 0 is unmatchable and silently - // drops the label. Skip it rather than store it. - if stream_num != 0 { - map.insert(info_id, stream_num); - } + ) && let Ok(stream_num) = stream_id_str.parse::() + { + // Stream numbers are 1-based per the apply_labels + // contract; a mapped 0 is unmatchable and silently + // drops the label. Skip it rather than store it. + if stream_num != 0 { + map.insert(info_id, stream_num); } } from = end; diff --git a/src/labels/ctrm.rs b/src/labels/ctrm.rs index 8398750..16cfb9d 100644 --- a/src/labels/ctrm.rs +++ b/src/labels/ctrm.rs @@ -50,10 +50,10 @@ fn merge(ls: Vec, mb: Vec) -> Vec { if let Some(mb_match) = mb .iter() .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 diff --git a/src/labels/dbp.rs b/src/labels/dbp.rs index 6ae6010..8054622 100644 --- a/src/labels/dbp.rs +++ b/src/labels/dbp.rs @@ -114,13 +114,13 @@ fn collect_textfield( if let Ok(n) = rest.parse::() { audios.insert(n, label.to_string()); } - } else if let Some(rest) = kind_n.strip_prefix("Subtitle") { - if let Ok(n) = rest.parse::() { - // Subtitle0 is conventionally the "None / Off" disable - // button, not an actual subtitle stream. - if n > 0 { - subs.insert(n, label.to_string()); - } + } else if let Some(rest) = kind_n.strip_prefix("Subtitle") + && let Ok(n) = rest.parse::() + { + // Subtitle0 is conventionally the "None / Off" disable + // button, not an actual subtitle stream. + if n > 0 { + subs.insert(n, label.to_string()); } } } diff --git a/src/labels/deluxe.rs b/src/labels/deluxe.rs index e7d5799..015e544 100644 --- a/src/labels/deluxe.rs +++ b/src/labels/deluxe.rs @@ -596,14 +596,13 @@ impl<'a> BindingDecoder<'a> { // Underneath the args: the object the constructor // operates on. For our pattern it's NewObj(X). let receiver = self.stack.pop().unwrap_or(StackVal::Unknown); - if let StackVal::NewObj(name) = receiver { - if name == member.class_name { + if let StackVal::NewObj(name) = receiver + && name == member.class_name { self.constructions.push(Construction { binding_type: name, args, }); } - } } // invokevirtual / invokestatic / invokeinterface — pop // args per descriptor, push a return placeholder unless diff --git a/src/labels/mod.rs b/src/labels/mod.rs index fc3c46f..2c7101d 100644 --- a/src/labels/mod.rs +++ b/src/labels/mod.rs @@ -574,10 +574,10 @@ fn extract(reader: &mut dyn SectorSource, udf: &UdfFs) -> Vec { // so the user sees every track even when only the "interesting" // ones have editorial names. Skips the merge when mpls_universal // was itself the chosen parser (its labels ARE the labels). - if name != "mpls_universal" { - if let Some(mpls_result) = mpls_universal::parse(reader, udf) { - fill_gaps_from_mpls(&mut labels, &mpls_result.labels); - } + if name != "mpls_universal" + && let Some(mpls_result) = mpls_universal::parse(reader, udf) + { + fill_gaps_from_mpls(&mut labels, &mpls_result.labels); } // CLPI orphan streams: PIDs in /BDMV/CLIPINF/*.clpi ProgramInfo diff --git a/src/labels/paramount.rs b/src/labels/paramount.rs index c26aedb..aa6a30d 100644 --- a/src/labels/paramount.rs +++ b/src/labels/paramount.rs @@ -142,10 +142,10 @@ fn find_feature_playlist(text: &str) -> Option { let element = &text[start..end]; // Prefer name="Feature" explicitly. - if let Some(name) = xml::attr(element, "name") { - if name.eq_ignore_ascii_case("Feature") { - return Some(element.to_string()); - } + if let Some(name) = xml::attr(element, "name") + && name.eq_ignore_ascii_case("Feature") + { + return Some(element.to_string()); } // Otherwise pick the one with the most audio streams. Count only diff --git a/src/labels/png_filenames.rs b/src/labels/png_filenames.rs index b66aa78..7d92292 100644 --- a/src/labels/png_filenames.rs +++ b/src/labels/png_filenames.rs @@ -41,10 +41,10 @@ pub fn parse(_reader: &mut dyn SectorSource, udf: &UdfFs) -> Option fn labels_from_filenames(names: &[String]) -> Vec { let mut seen: Vec<&'static str> = Vec::new(); for name in names { - if let Some(code) = filename_lang(name) { - if !seen.contains(&code) { - seen.push(code); - } + if let Some(code) = filename_lang(name) + && !seen.contains(&code) + { + seen.push(code); } } seen.into_iter() diff --git a/src/mpls.rs b/src/mpls.rs index 4d37542..7d864ba 100644 --- a/src/mpls.rs +++ b/src/mpls.rs @@ -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). // IG is parsed only to advance spos and is then discarded by the // 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(); } - } _ => {} } diff --git a/src/mux/codec/ac3.rs b/src/mux/codec/ac3.rs index 0389b67..56aec6b 100644 --- a/src/mux/codec/ac3.rs +++ b/src/mux/codec/ac3.rs @@ -195,11 +195,11 @@ impl Ac3Parser { // First access unit that starts in this PES's own bytes: adopt // this PES's timestamp so a genuine PTS jump is followed instead // of the running cadence drifting past it. - if let Some(a) = &anchor { - if start >= a.at { - frame_pts_ns = a.pts_ns; - anchor = None; - } + if let Some(a) = &anchor + && start >= a.at + { + frame_pts_ns = a.pts_ns; + anchor = None; } let duration_ns = frame_duration_ns(remaining, bsid); pending = Some(PendingAu { diff --git a/src/mux/codec/h264.rs b/src/mux/codec/h264.rs index d16a8a5..ebcf7bb 100644 --- a/src/mux/codec/h264.rs +++ b/src/mux/codec/h264.rs @@ -365,17 +365,17 @@ impl CodecParser for H264Parser { const HIGH_PROFILES: [u8; 14] = [ 100, 110, 122, 144, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135, ]; - if HIGH_PROFILES.contains(&profile_idc) { - if 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 1: 11111xxx — reserved(5) + bit_depth_luma_minus8(3) - record.push(0xF8 | (depth_luma & 0x07)); - // byte 2: 11111xxx — reserved(5) + bit_depth_chroma_minus8(3) - record.push(0xF8 | (depth_chroma & 0x07)); - // byte 3: num_of_sequence_parameter_set_ext (0 = none) - record.push(0x00); - } + if HIGH_PROFILES.contains(&profile_idc) + && 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 1: 11111xxx — reserved(5) + bit_depth_luma_minus8(3) + record.push(0xF8 | (depth_luma & 0x07)); + // byte 2: 11111xxx — reserved(5) + bit_depth_chroma_minus8(3) + record.push(0xF8 | (depth_chroma & 0x07)); + // byte 3: num_of_sequence_parameter_set_ext (0 = none) + record.push(0x00); } Some(record) diff --git a/src/mux/codec/hevc.rs b/src/mux/codec/hevc.rs index cbcae38..69634ea 100644 --- a/src/mux/codec/hevc.rs +++ b/src/mux/codec/hevc.rs @@ -307,11 +307,10 @@ impl HevcParser { }; let rbsp = strip_emulation_prevention(raw); let mut i = 0usize; - loop { - // payloadType: sum of 0xFF run + final byte. - let Some(payload_type) = read_sei_ff_value(&rbsp, &mut i) else { - break; - }; + // payloadType: sum of 0xFF run + final byte. Exhausting the RBSP ends the + // walk; the remaining `let ... else break` arms below handle a TRUNCATED + // message, which is a different condition from a clean end. + while let Some(payload_type) = read_sei_ff_value(&rbsp, &mut i) { // payloadSize: same ff-extension coding. let Some(payload_size) = read_sei_ff_value(&rbsp, &mut i) else { break; @@ -504,11 +503,11 @@ impl CodecParser for HevcParser { // 33-bit counter wrapped: add another period and re-check, rather // than treat the wrap as a backward clip reset. let mut unwrapped = raw_pts + self.pts_wrap_offset; - if let Some(high) = self.high_pts { - if high - unwrapped > PTS_WRAP_PERIOD / 2 { - self.pts_wrap_offset += PTS_WRAP_PERIOD; - unwrapped += PTS_WRAP_PERIOD; - } + if let Some(high) = self.high_pts + && high - unwrapped > PTS_WRAP_PERIOD / 2 + { + self.pts_wrap_offset += PTS_WRAP_PERIOD; + unwrapped += PTS_WRAP_PERIOD; } match self.high_pts { 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 // `slice_type` — is EXACT. With no PPS we decline rather than // guess, leaving coding `None` (honestly absent). - if coding_type.is_none() && nal_type <= NAL_VCL_MAX { - if let Some(num_extra) = self + if coding_type.is_none() + && nal_type <= NAL_VCL_MAX + && let Some(num_extra) = self .cur_pps .as_deref() .and_then(hevc_num_extra_slice_header_bits) - { - coding_type = hevc_first_slice_coding_type( - &data[nal_start..end], - nal_type, - num_extra, - ); - } + { + coding_type = hevc_first_slice_coding_type( + &data[nal_start..end], + nal_type, + num_extra, + ); } match nal_type { diff --git a/src/mux/codec/mpeg2.rs b/src/mux/codec/mpeg2.rs index 7cacdfc..4727c96 100644 --- a/src/mux/codec/mpeg2.rs +++ b/src/mux/codec/mpeg2.rs @@ -196,10 +196,10 @@ impl Mpeg2Parser { if let Some(h) = extract_seq_header(&data) { self.progressive_sequence = parse_progressive_sequence(&h); self.seq_header = Some(h); - if let Some((num, den)) = self.frame_rate() { - if num > 0 { - self.frame_duration_ns = 1_000_000_000i64 * den as i64 / num as i64; - } + if let Some((num, den)) = self.frame_rate() + && num > 0 + { + 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, diff --git a/src/mux/codec/reorder.rs b/src/mux/codec/reorder.rs index 34e7a56..01fd532 100644 --- a/src/mux/codec/reorder.rs +++ b/src/mux/codec/reorder.rs @@ -168,12 +168,12 @@ impl SparsePtsReorder { // (assumes both anchors sit at a similar relative display slot), // but each GOP re-locks its own origin, so the estimate only sets // intra-GOP spacing. - if self.dur_ns == 0 { - if let (Some((p_held, _)), Some((p_next, _))) = (held.anchor, gop.anchor) { - let span = p_next - p_held; - if span > 0 && held.count > 0 { - self.dur_ns = (span / held.count).max(1); - } + if self.dur_ns == 0 + && let (Some((p_held, _)), Some((p_next, _))) = (held.anchor, gop.anchor) + { + let span = p_next - p_held; + if span > 0 && held.count > 0 { + self.dur_ns = (span / held.count).max(1); } } out = self.emit_gop(held); diff --git a/src/mux/codec/truehd.rs b/src/mux/codec/truehd.rs index 4d70b33..d07b58e 100644 --- a/src/mux/codec/truehd.rs +++ b/src/mux/codec/truehd.rs @@ -344,49 +344,49 @@ impl CodecParser for TrueHdParser { // 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 // the next PES legitimately begins a new AU and seeds the base. - if self.buf.is_empty() { - if 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` - // 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: - // - // 1. Small backward jitter (sub-second PES rounding): when the - // buffer empties exactly on a PES boundary and that PES's PTS - // lands a few ticks *below* the running cadence, an - // unconditional reset would set the next AU's timestamp below - // the AU just emitted, producing non-monotonic block - // 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 - // concatenated stream and a non-seamless boundary resets the - // source PES PTS near zero. This is NOT jitter and must NOT be - // clamped: clamping strands the audio at the previous clip's - // tail cadence, so when `TimelineContinuity` later bumps the - // global offset for the new epoch (driven by the video - // back-jump) the stranded-high audio PTS is flung ~a whole - // clip past the frontier, producing the non-monotonic - // audio-DTS band on multi-clip titles (Dune: Part Two, Top - // Gun). ADOPT the raw reset so the per-track raw PTS that - // reaches `TimelineContinuity` carries the true boundary, and - // the corrector rebases it exactly as it already does for the - // 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()`. - let new = pts_to_ns(pts); - if new < self.next_pts_ns - DISCONTINUITY_BACKSTEP_NS { - // Clip-boundary reset: take the raw PTS, restart the cadence. - self.next_pts_ns = new; - } else { - // Within-clip jitter (or forward progression): stay monotonic. - self.next_pts_ns = self.next_pts_ns.max(new); - } + if self.buf.is_empty() + && 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` + // 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: + // + // 1. Small backward jitter (sub-second PES rounding): when the + // buffer empties exactly on a PES boundary and that PES's PTS + // lands a few ticks *below* the running cadence, an + // unconditional reset would set the next AU's timestamp below + // the AU just emitted, producing non-monotonic block + // 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 + // concatenated stream and a non-seamless boundary resets the + // source PES PTS near zero. This is NOT jitter and must NOT be + // clamped: clamping strands the audio at the previous clip's + // tail cadence, so when `TimelineContinuity` later bumps the + // global offset for the new epoch (driven by the video + // back-jump) the stranded-high audio PTS is flung ~a whole + // clip past the frontier, producing the non-monotonic + // audio-DTS band on multi-clip titles (Dune: Part Two, Top + // Gun). ADOPT the raw reset so the per-track raw PTS that + // reaches `TimelineContinuity` carries the true boundary, and + // the corrector rebases it exactly as it already does for the + // 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()`. + let new = pts_to_ns(pts); + if new < self.next_pts_ns - DISCONTINUITY_BACKSTEP_NS { + // Clip-boundary reset: take the raw PTS, restart the cadence. + self.next_pts_ns = new; + } else { + // Within-clip jitter (or forward progression): stay monotonic. + self.next_pts_ns = self.next_pts_ns.max(new); } } diff --git a/src/mux/codec/vc1.rs b/src/mux/codec/vc1.rs index 8d77c90..8769dd4 100644 --- a/src/mux/codec/vc1.rs +++ b/src/mux/codec/vc1.rs @@ -216,12 +216,11 @@ impl CodecParser for Vc1Parser { let end = find_next_sc(data, i + 4).unwrap_or(data.len()); let sh = &data[i..end]; // Try to parse resolution from advanced profile sequence header - if self.seq_header.is_none() { - if let Some((w, h)) = parse_vc1_resolution(sh) { + if self.seq_header.is_none() + && let Some((w, h)) = parse_vc1_resolution(sh) { self.width = w; self.height = h; } - } // Collect into a scratch Vec so handle_header can // append; we discard the Vec and only keep the flag. let mut scratch = Vec::new(); @@ -250,12 +249,11 @@ impl CodecParser for Vc1Parser { } has_entry_point = true; } - SC_FRAME => { + SC_FRAME // Frame data starts at this start code - if frame_start.is_none() { + if frame_start.is_none() => { frame_start = Some(i); } - } _ => {} } i += 4; diff --git a/src/mux/disc.rs b/src/mux/disc.rs index a334542..b5a80a1 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -512,16 +512,16 @@ impl DiscStream { // priority, mirroring the multipass sweep's transport-failure rule // in `read_error::handle_read_error`. The CLI/UX surfaces this so the // user power-cycles the drive (or switches to multipass recovery). - if let Some(e) = res.as_ref().err() { - if e.is_scsi_transport_failure() { - let (status, sense) = extract_scsi_context(e); - return Err(crate::error::Error::DiscRead { - sector: lba as u64, - status: Some(status), - sense, - } - .into()); + if let Some(e) = res.as_ref().err() + && e.is_scsi_transport_failure() + { + let (status, sense) = extract_scsi_context(e); + return Err(crate::error::Error::DiscRead { + sector: lba as u64, + status: Some(status), + sense, } + .into()); } if (sectors as u32) <= align { @@ -571,16 +571,16 @@ impl DiscStream { // a dead bridge as a skippable unit and marching the whole disc // at one bridge-recovery per probe (hard rule #2, "runs forever, // no MKV"). Re-check `rec` and abort, mirroring line 442. - if let Some(e) = rec.as_ref().err() { - if e.is_scsi_transport_failure() { - let (status, sense) = extract_scsi_context(e); - return Err(crate::error::Error::DiscRead { - sector: lba as u64, - status: Some(status), - sense, - } - .into()); + if let Some(e) = rec.as_ref().err() + && e.is_scsi_transport_failure() + { + let (status, sense) = extract_scsi_context(e); + return Err(crate::error::Error::DiscRead { + sector: lba as u64, + status: Some(status), + sense, } + .into()); } // 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() { if let Some((_, track)) = self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) - { - if let Some((_, parser)) = + && let Some((_, parser)) = self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) - { - let resync = &mut self.resync; - let is_video = &self.is_video; - let pending = &mut self.pending_frames; - for frame in parser.parse(pes) { - // Same B1 gate — a concealed gap can leave a - // post-gap frame in the demuxer's final flush. - let emit = match resync.get_mut(*track) { - Some(gate) => gate.admit( - is_video.get(*track).copied().unwrap_or(false), - frame.discontinuity, - frame.keyframe, - ), - None => true, - }; - if emit { - pending.push_back(crate::pes::PesFrame::from_codec_frame( - *track, frame, - )); - } + { + let resync = &mut self.resync; + let is_video = &self.is_video; + let pending = &mut self.pending_frames; + for frame in parser.parse(pes) { + // Same B1 gate — a concealed gap can leave a + // post-gap frame in the demuxer's final flush. + let emit = match resync.get_mut(*track) { + Some(gate) => gate.admit( + is_video.get(*track).copied().unwrap_or(false), + frame.discontinuity, + frame.keyframe, + ), + None => true, + }; + if emit { + pending.push_back(crate::pes::PesFrame::from_codec_frame( + *track, frame, + )); } } } @@ -1021,10 +1019,11 @@ impl crate::pes::Stream for DiscStream { return true; } for (idx, s) in self.title.streams.iter().enumerate() { - if let crate::disc::Stream::Video(v) = s { - if !v.secondary && self.codec_private(idx).is_none() { - return false; - } + if let crate::disc::Stream::Video(v) = s + && !v.secondary + && self.codec_private(idx).is_none() + { + return false; } } true diff --git a/src/mux/m2ts_mux/mod.rs b/src/mux/m2ts_mux/mod.rs index 11ac310..34d5319 100644 --- a/src/mux/m2ts_mux/mod.rs +++ b/src/mux/m2ts_mux/mod.rs @@ -436,7 +436,7 @@ impl M2tsMux { } 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_pmt()?; } diff --git a/src/mux/mkv.rs b/src/mux/mkv.rs index 4423ea2..9df12f5 100644 --- a/src/mux/mkv.rs +++ b/src/mux/mkv.rs @@ -1583,10 +1583,11 @@ impl MkvMuxer { // track: the ReferenceBlock above is emitted for any video track, so a // single global slot produced cross-track references on a multi-video-track // title (MVC base + secondary view, or a disc with two angles). - if keyframe && is_video { - if let Some(slot) = self.last_video_keyframe_ticks.get_mut(track_idx) { - *slot = Some(pts_ticks); - } + if keyframe + && is_video + && let Some(slot) = self.last_video_keyframe_ticks.get_mut(track_idx) + { + *slot = Some(pts_ticks); } self.frame_count += 1; @@ -1600,29 +1601,29 @@ impl MkvMuxer { // (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 // patch is a single-byte in-place rewrite (then restore position). - if let Some(fixup) = self.ac3_channel_fixups.get_mut(&track_idx) { - if !fixup.corrected { - match super::codec::ac3::acmod_channels(data) { - Some(actual) if actual > 0 => { - if actual != fixup.claimed { - tracing::warn!( - target: "mux", - "AC-3 track {track_idx}: IFO claimed {} channels but bitstream acmod says {}; trusting the bitstream (possible wrong-stream selection)", - fixup.claimed, - actual, - ); - let here = self.writer.stream_position()?; - self.writer - .seek(std::io::SeekFrom::Start(fixup.value_offset))?; - self.writer.write_all(&[actual])?; - self.writer.seek(std::io::SeekFrom::Start(here))?; - } - fixup.corrected = true; + if let Some(fixup) = self.ac3_channel_fixups.get_mut(&track_idx) + && !fixup.corrected + { + match super::codec::ac3::acmod_channels(data) { + Some(actual) if actual > 0 => { + if actual != fixup.claimed { + tracing::warn!( + target: "mux", + "AC-3 track {track_idx}: IFO claimed {} channels but bitstream acmod says {}; trusting the bitstream (possible wrong-stream selection)", + fixup.claimed, + actual, + ); + let here = self.writer.stream_position()?; + self.writer + .seek(std::io::SeekFrom::Start(fixup.value_offset))?; + self.writer.write_all(&[actual])?; + self.writer.seek(std::io::SeekFrom::Start(here))?; } - // Frame too short to carry the BSI bits — keep the passed - // (IFO) value and try again on the next frame. - _ => {} + fixup.corrected = true; } + // 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 MkvMuxer { // with a 1-byte size VINT covering the remaining 19 bytes occupies // exactly 1 + 1 + 19 = 21 bytes, overwriting the entry in place without // shifting any following element. - if !have_cues { - if let Some(entry_pos) = self.cues_seek_entry_pos { - self.writer.seek(std::io::SeekFrom::Start(entry_pos))?; - ebml::write_id(&mut self.writer, ebml::VOID)?; - // 19 = 21-byte entry minus the Void ID (1) and size (1) bytes. - ebml::write_size(&mut self.writer, 19)?; - self.writer.write_all(&[0u8; 19])?; - } + if !have_cues && let Some(entry_pos) = self.cues_seek_entry_pos { + self.writer.seek(std::io::SeekFrom::Start(entry_pos))?; + ebml::write_id(&mut self.writer, ebml::VOID)?; + // 19 = 21-byte entry minus the Void ID (1) and size (1) bytes. + ebml::write_size(&mut self.writer, 19)?; + self.writer.write_all(&[0u8; 19])?; } self.writer.seek(std::io::SeekFrom::End(0))?; diff --git a/src/mux/mkvstream.rs b/src/mux/mkvstream.rs index f1599ee..330dcad 100644 --- a/src/mux/mkvstream.rs +++ b/src/mux/mkvstream.rs @@ -254,11 +254,11 @@ impl MvcMerge { .front() .map(|pb| pb.additional.is_some()) .unwrap_or(false); - if front_ready || self.pending_base.len() > MVC_PAIR_WINDOW { - if let Some(pb) = self.pending_base.pop_front() { - out.push((pb.frame, pb.additional)); - continue; - } + if (front_ready || self.pending_base.len() > MVC_PAIR_WINDOW) + && let Some(pb) = self.pending_base.pop_front() + { + out.push((pb.frame, pb.additional)); + continue; } break; } diff --git a/src/mux/mp4/audio.rs b/src/mux/mp4/audio.rs index 7812d60..64dddf3 100644 --- a/src/mux/mp4/audio.rs +++ b/src/mux/mp4/audio.rs @@ -159,11 +159,9 @@ fn parse_eac3(f: &[u8]) -> Option { // samples at sample_rate. rate = bytes·8·sr / samples / 1000. let frame_bytes = (frmsiz as u64 + 1) * 2; let samples = blocks as u64 * 256; - let data_rate_kbps = if samples > 0 { - ((frame_bytes * 8 * sample_rate as u64) / samples / 1000) as u16 - } else { - 0 - }; + let data_rate_kbps = (frame_bytes * 8 * sample_rate as u64) + .checked_div(samples) + .map_or(0, |r| (r / 1000) as u16); Some(DolbyConfig { fscod, diff --git a/src/mux/mp4/mod.rs b/src/mux/mp4/mod.rs index b048f5b..92918f2 100644 --- a/src/mux/mp4/mod.rs +++ b/src/mux/mp4/mod.rs @@ -285,11 +285,13 @@ impl Mp4Sink { let mut tracks = Vec::new(); let mut route = vec![None; title.streams.len()]; - let mut next_id = 1u32; let mut video_codec = Codec::Hevc; - for &i in &report.included { - let track_id = next_id; - next_id += 1; + // Track ids are 1-based and assigned in inclusion order. `moov`'s + // next_track_id is NOT derived from this counter — it is max(track_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()); match &title.streams[i] { DiscStream::Video(v) => { @@ -440,10 +442,11 @@ impl Stream for Mp4Sink { // cost us the frame. Dropping leading frames here lost audio silently, and // a track whose frames never parsed vanished from the output entirely with // no report; finish() now decides that case loudly instead. - if self.tracks[slot].media == Media::Audio && self.tracks[slot].audio_entry.is_none() { - if let Some(entry) = audio::dolby_sample_entry(self.tracks[slot].codec, &frame.data) { - self.tracks[slot].audio_entry = Some(entry); - } + if self.tracks[slot].media == Media::Audio + && self.tracks[slot].audio_entry.is_none() + && 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 offset = self.mdat_start + 16 + self.mdat_payload; diff --git a/src/mux/pipelined_stream.rs b/src/mux/pipelined_stream.rs index f4273be..90cd8d1 100644 --- a/src/mux/pipelined_stream.rs +++ b/src/mux/pipelined_stream.rs @@ -420,10 +420,11 @@ impl Stream for PipelinedPesStream { return true; } for (idx, s) in self.title.streams.iter().enumerate() { - if let crate::disc::Stream::Video(v) = s { - if !v.secondary && self.codec_private(idx).is_none() { - return false; - } + if let crate::disc::Stream::Video(v) = s + && !v.secondary + && self.codec_private(idx).is_none() + { + return false; } } true diff --git a/src/mux/resolve.rs b/src/mux/resolve.rs index c271c33..6361deb 100644 --- a/src/mux/resolve.rs +++ b/src/mux/resolve.rs @@ -193,10 +193,10 @@ pub fn parse_url(url: &str) -> StreamUrl { return StreamUrl::Null; } } - if let Some(rest) = url.strip_prefix("stdio://") { - if rest.is_empty() { - return StreamUrl::Stdio; - } + if let Some(rest) = url.strip_prefix("stdio://") + && rest.is_empty() + { + return StreamUrl::Stdio; } if let Some(rest) = url.strip_prefix("iso://") { return StreamUrl::Iso { @@ -1646,20 +1646,19 @@ pub(crate) fn resolve_mux_key_map_cached( _ => Vec::new(), }; let mut idx = pick(&samples, &pool); - if idx.is_none() { - if let Some(f) = fetch { - if !samples.is_empty() { - let fresh = f.unit_keys(&samples); - if let crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } = keys { - for k in fresh { - if !unit_keys.iter().any(|(_, h)| *h == k) { - let i = unit_keys.len() as u32; - unit_keys.push((i, k)); - } - } - idx = pick(&samples, unit_keys); + if idx.is_none() + && let Some(f) = fetch + && !samples.is_empty() + { + let fresh = f.unit_keys(&samples); + if let crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } = keys { + for k in fresh { + if !unit_keys.iter().any(|(_, h)| *h == k) { + let i = unit_keys.len() as u32; + unit_keys.push((i, k)); } } + idx = pick(&samples, unit_keys); } } // `sample_units` draws REAL content (not authored-bad units), so a sample diff --git a/src/mux/stdio.rs b/src/mux/stdio.rs index 0c71420..c43fcb5 100644 --- a/src/mux/stdio.rs +++ b/src/mux/stdio.rs @@ -53,12 +53,12 @@ impl StdioStream { /// zero-frame output stream still emits the magic + metadata header, /// keeping the wire protocol symmetric with the read side's read_header(). fn ensure_header_written(&mut self) -> io::Result<()> { - if let Some(w) = &mut self.writer { - if !self.header_written { - let m = meta::M2tsMeta::from_title(&self.disc_title); - meta::write_header(w, &m)?; - self.header_written = true; - } + if let Some(w) = &mut self.writer + && !self.header_written + { + let m = meta::M2tsMeta::from_title(&self.disc_title); + meta::write_header(w, &m)?; + self.header_written = true; } Ok(()) } diff --git a/src/mux/timeline.rs b/src/mux/timeline.rs index 1819b5f..29d077e 100644 --- a/src/mux/timeline.rs +++ b/src/mux/timeline.rs @@ -140,14 +140,14 @@ impl TimelineContinuity { // overflow: a panic out of the public `Stream::write` path in an // overflow-checked build, and in release a wrap to the opposite sign // that fires the straggler clamp on essentially every passive frame. - if let Some(high) = self.high_ns { - if mapped > high.saturating_add(DISCONTINUITY_BACKSTEP_NS) { - let prev_mapped = raw_pts_ns.saturating_add(self.prev_offset_ns); - if prev_mapped <= high - && prev_mapped >= high.saturating_sub(DISCONTINUITY_BACKSTEP_NS) - { - return prev_mapped; - } + if let Some(high) = self.high_ns + && mapped > high.saturating_add(DISCONTINUITY_BACKSTEP_NS) + { + let prev_mapped = raw_pts_ns.saturating_add(self.prev_offset_ns); + if prev_mapped <= high + && prev_mapped >= high.saturating_sub(DISCONTINUITY_BACKSTEP_NS) + { + return prev_mapped; } } return mapped; diff --git a/src/progress.rs b/src/progress.rs index d49ae45..b466a64 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -233,7 +233,7 @@ impl Heartbeat { /// iterations. Otherwise identical to [`tick`](Heartbeat::tick). pub fn tick_cpu(&mut self, pos: u64, total: u64) -> bool { 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; } self.tick(pos, total) diff --git a/src/sector/prefetched.rs b/src/sector/prefetched.rs index 6d8ce4e..9b7accb 100644 --- a/src/sector/prefetched.rs +++ b/src/sector/prefetched.rs @@ -227,7 +227,9 @@ impl PrefetchedSectorSource { // way to hand the decrypt step an aligned chunk — // surface a typed error instead of emitting // 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())); return; } diff --git a/src/udf.rs b/src/udf.rs index 2219643..3f63ef0 100644 --- a/src/udf.rs +++ b/src/udf.rs @@ -1264,10 +1264,10 @@ fn parse_dstring(data: &[u8]) -> String { for i in (0..chars.len()).step_by(2) { if i + 1 < chars.len() { let c = ((chars[i] as u16) << 8) | chars[i + 1] as u16; - if c != 0 { - if let Some(ch) = char::from_u32(c as u32) { - s.push(ch); - } + if c != 0 + && let Some(ch) = char::from_u32(c as u32) + { + s.push(ch); } } }