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:
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -77,12 +77,12 @@ 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: ") {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn git_short_hash() -> Option<String> {
|
||||
let out = std::process::Command::new("git")
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -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));
|
||||
|
||||
+1
-3
@@ -328,11 +328,9 @@ fn parse_program_info(data: &[u8]) -> Vec<ClpiStream> {
|
||||
}
|
||||
}
|
||||
// PG, IG: coding_type + 3-byte language [+ char_code for PG]
|
||||
c::PG | c::IG => {
|
||||
if sci.len() >= 4 {
|
||||
c::PG | c::IG if sci.len() >= 4 => {
|
||||
language = String::from_utf8_lossy(&sci[1..4]).to_string();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -249,11 +249,11 @@ 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() {
|
||||
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
|
||||
// we emit the crack-specific context (tried/lba/extent_idx).
|
||||
@@ -367,8 +367,9 @@ 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[..] {
|
||||
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);
|
||||
@@ -379,7 +380,6 @@ pub fn descramble_region(buf: &mut [u8], title_key: &mut [u8; 5]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a sector has the CSS scramble flag set.
|
||||
///
|
||||
|
||||
+3
-3
@@ -332,12 +332,12 @@ impl AacsKeyMap {
|
||||
if sectors == 0 {
|
||||
return;
|
||||
}
|
||||
if let Some(last) = plan.last_mut() {
|
||||
if last.start_lba.saturating_add(last.sector_count) == lba {
|
||||
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,
|
||||
sector_count: sectors,
|
||||
|
||||
+9
-10
@@ -29,8 +29,8 @@ 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);
|
||||
@@ -38,7 +38,6 @@ impl Disc {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
titles
|
||||
}
|
||||
|
||||
@@ -92,8 +91,9 @@ 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) {
|
||||
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
|
||||
@@ -144,7 +144,6 @@ impl Disc {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clips.push(Clip {
|
||||
clip_id: play_item.clip_id.clone(),
|
||||
@@ -266,11 +265,12 @@ 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()
|
||||
@@ -284,7 +284,6 @@ impl Disc {
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert marks to chapters. mark_type == 1 is an entry-mark
|
||||
// (chapter); type 2 is a link point and type 0 is reserved, so
|
||||
|
||||
@@ -104,11 +104,11 @@ fn max_substream_channels(data: &[u8]) -> Option<u8> {
|
||||
};
|
||||
let start = pos + rel;
|
||||
let frame = &data[start..];
|
||||
if let Some(ch) = ac3::acmod_channels(frame) {
|
||||
if ch > 0 {
|
||||
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.
|
||||
let size = ac3::ac3_frame_size(frame);
|
||||
|
||||
+10
-9
@@ -165,14 +165,14 @@ 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 {
|
||||
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
|
||||
// parent. The root itself already exists (create_dir_all above).
|
||||
@@ -388,13 +388,13 @@ 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 {
|
||||
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());
|
||||
plan_tree(
|
||||
@@ -756,12 +756,13 @@ 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') {
|
||||
if let Some(rest) = up.strip_prefix(prefix)
|
||||
&& rest.len() == 1
|
||||
&& matches!(rest.as_bytes()[0], b'1'..=b'9')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -305,15 +305,15 @@ fn collect_es(
|
||||
}
|
||||
}
|
||||
PRIVATE_STREAM_1 => {
|
||||
if let Some(sub) = pkt.sub_stream_id {
|
||||
if (0xC0..=0xC7).contains(&sub) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -687,16 +687,16 @@ 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<u32>, Option<f64>) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
(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,8 +3049,9 @@ 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::<u32>() {
|
||||
if let Ok(content) = std::fs::read_to_string(&sysfs_path)
|
||||
&& 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;
|
||||
// Align down to 3 (one aligned unit)
|
||||
@@ -3060,7 +3061,6 @@ pub fn detect_max_batch_sectors(device_path: &str) -> u16 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DEFAULT_BATCH_SECTORS_OPTICAL
|
||||
} else {
|
||||
DEFAULT_BATCH_SECTORS_BLOCK
|
||||
|
||||
@@ -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,15 +393,14 @@ fn verdicts(evidence: &HashMap<u16, TrackEvidence>, conclusive: bool) -> HashMap
|
||||
/// from the map was never observed and keeps its vendor-derived flag.
|
||||
fn apply_verdicts(title: &mut DiscTitle, verdicts: &HashMap<u16, bool>) {
|
||||
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) {
|
||||
if let Stream::Subtitle(sub) = s
|
||||
&& sub.codec == Codec::Pgs
|
||||
&& let Some(&forced) = verdicts.get(&sub.pid)
|
||||
{
|
||||
sub.forced = forced;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
+2
-3
@@ -25,14 +25,13 @@ 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()
|
||||
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(_) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ pub fn parse_hex_bytes(s: &str) -> Option<Vec<u8>> {
|
||||
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);
|
||||
|
||||
+3
-3
@@ -133,11 +133,11 @@ where
|
||||
match rx.recv_timeout(slice) {
|
||||
Ok(v) => return Ok(v),
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
if let Some(h) = halt {
|
||||
if h.is_cancelled() {
|
||||
if let Some(h) = halt
|
||||
&& h.is_cancelled()
|
||||
{
|
||||
return Err(BoundedError::Halted);
|
||||
}
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return Err(BoundedError::Timeout);
|
||||
}
|
||||
|
||||
+3
-3
@@ -752,8 +752,9 @@ impl<I: Send + 'static, R: Send + 'static> Pipeline<I, R> {
|
||||
Err(payload) => Err(consumer_panicked(payload)),
|
||||
};
|
||||
}
|
||||
if let Some(h) = halt {
|
||||
if h.is_cancelled() {
|
||||
if let Some(h) = halt
|
||||
&& h.is_cancelled()
|
||||
{
|
||||
return finish_with_grace(
|
||||
handle,
|
||||
&state,
|
||||
@@ -761,7 +762,6 @@ impl<I: Send + 'static, R: Send + 'static> Pipeline<I, R> {
|
||||
Error::Halted,
|
||||
);
|
||||
}
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return finish_with_grace(
|
||||
handle,
|
||||
|
||||
+9
-9
@@ -100,12 +100,12 @@ pub fn parse(reader: &mut dyn SectorSource, udf: &UdfFs) -> Option<DiscMetadata>
|
||||
// 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 {
|
||||
if out.disc_number.is_none()
|
||||
&& let Some(ds) = disc_set
|
||||
{
|
||||
out.disc_number = Some(ds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if out.titles.is_empty() {
|
||||
None
|
||||
@@ -184,22 +184,22 @@ fn extract_title(xml_text: &str) -> Option<String> {
|
||||
// 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() {
|
||||
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 <titleName> 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() {
|
||||
if let Some(t) = xml::text(block, "titleName")
|
||||
&& !t.is_empty()
|
||||
{
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -36,11 +36,11 @@ pub fn parse(reader: &mut dyn SectorSource, udf: &UdfFs) -> Option<ParseResult>
|
||||
|
||||
// Stream number mapping from playbackconfig.xml
|
||||
let mut stream_map: HashMap<String, u16> = 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) {
|
||||
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,8 +189,8 @@ fn parse_playback_config(text: &str, map: &mut HashMap<String, u16>) {
|
||||
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::<u16>() {
|
||||
) && let Ok(stream_num) = stream_id_str.parse::<u16>()
|
||||
{
|
||||
// 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.
|
||||
@@ -198,7 +198,6 @@ fn parse_playback_config(text: &str, map: &mut HashMap<String, u16>) {
|
||||
map.insert(info_id, stream_num);
|
||||
}
|
||||
}
|
||||
}
|
||||
from = end;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -50,12 +50,12 @@ fn merge(ls: Vec<StreamLabel>, mb: Vec<StreamLabel>) -> Vec<StreamLabel> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Append any menu_base-only stream (present in mb but not in ls by
|
||||
// (stream_type, stream_number)). Without this the both-files path
|
||||
// silently drops streams the menu_base-only path would have emitted:
|
||||
|
||||
+3
-3
@@ -114,8 +114,9 @@ fn collect_textfield(
|
||||
if let Ok(n) = rest.parse::<u16>() {
|
||||
audios.insert(n, label.to_string());
|
||||
}
|
||||
} else if let Some(rest) = kind_n.strip_prefix("Subtitle") {
|
||||
if let Ok(n) = rest.parse::<u16>() {
|
||||
} else if let Some(rest) = kind_n.strip_prefix("Subtitle")
|
||||
&& let Ok(n) = rest.parse::<u16>()
|
||||
{
|
||||
// Subtitle0 is conventionally the "None / Off" disable
|
||||
// button, not an actual subtitle stream.
|
||||
if n > 0 {
|
||||
@@ -123,7 +124,6 @@ fn collect_textfield(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_label(num: u16, label: String, stream_type: StreamLabelType) -> StreamLabel {
|
||||
let lang_info = vocab::lang(&label);
|
||||
|
||||
@@ -596,15 +596,14 @@ 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
|
||||
// descriptor returns V (void).
|
||||
|
||||
+3
-3
@@ -574,11 +574,11 @@ fn extract(reader: &mut dyn SectorSource, udf: &UdfFs) -> Vec<StreamLabel> {
|
||||
// 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) {
|
||||
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
|
||||
// that no MPLS playlist references. Empirically a small fraction of
|
||||
|
||||
@@ -142,11 +142,11 @@ fn find_feature_playlist(text: &str) -> Option<String> {
|
||||
let element = &text[start..end];
|
||||
|
||||
// Prefer name="Feature" explicitly.
|
||||
if let Some(name) = xml::attr(element, "name") {
|
||||
if name.eq_ignore_ascii_case("Feature") {
|
||||
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
|
||||
// non-empty slots so a malformed `aud=",,,,,"` can't outscore a
|
||||
|
||||
@@ -41,12 +41,12 @@ pub fn parse(_reader: &mut dyn SectorSource, udf: &UdfFs) -> Option<ParseResult>
|
||||
fn labels_from_filenames(names: &[String]) -> Vec<StreamLabel> {
|
||||
let mut seen: Vec<&'static str> = Vec::new();
|
||||
for name in names {
|
||||
if let Some(code) = filename_lang(name) {
|
||||
if !seen.contains(&code) {
|
||||
if let Some(code) = filename_lang(name)
|
||||
&& !seen.contains(&code)
|
||||
{
|
||||
seen.push(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
seen.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, code)| StreamLabel {
|
||||
|
||||
+2
-3
@@ -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();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -195,12 +195,12 @@ 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 {
|
||||
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 {
|
||||
start,
|
||||
|
||||
@@ -365,8 +365,9 @@ 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) {
|
||||
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)
|
||||
@@ -376,7 +377,6 @@ impl CodecParser for H264Parser {
|
||||
// byte 3: num_of_sequence_parameter_set_ext (0 = none)
|
||||
record.push(0x00);
|
||||
}
|
||||
}
|
||||
|
||||
Some(record)
|
||||
}
|
||||
|
||||
+10
-11
@@ -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,12 +503,12 @@ 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 {
|
||||
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 => {
|
||||
self.pending_clip_boundary = true;
|
||||
@@ -564,8 +563,9 @@ 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)
|
||||
@@ -576,7 +576,6 @@ impl CodecParser for HevcParser {
|
||||
num_extra,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
match nal_type {
|
||||
NAL_VPS => {
|
||||
|
||||
@@ -196,12 +196,12 @@ 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 {
|
||||
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,
|
||||
// resetting temporal_reference to 0.
|
||||
let gop_boundary = find_code(&data, 0, GOP_CODE).is_some()
|
||||
|
||||
@@ -168,14 +168,14 @@ 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) {
|
||||
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);
|
||||
self.held = Some(gop);
|
||||
}
|
||||
|
||||
@@ -344,8 +344,9 @@ 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 {
|
||||
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
|
||||
@@ -388,7 +389,6 @@ impl CodecParser for TrueHdParser {
|
||||
self.next_pts_ns = self.next_pts_ns.max(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.buf.extend_from_slice(&pes.data);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+11
-12
@@ -512,8 +512,9 @@ 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() {
|
||||
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,
|
||||
@@ -522,7 +523,6 @@ impl DiscStream {
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
if (sectors as u32) <= align {
|
||||
// Bottomed out at one unit (AACS) / one sector (CSS) / the
|
||||
@@ -571,8 +571,9 @@ 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() {
|
||||
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,
|
||||
@@ -581,7 +582,6 @@ impl DiscStream {
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Recovery read also failed. Skip the WHOLE failed unit or bail.
|
||||
// Zero-filling and advancing by the full unit keeps
|
||||
@@ -725,8 +725,7 @@ 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;
|
||||
@@ -752,7 +751,6 @@ impl crate::pes::Stream for DiscStream {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// PS demuxer flush (DVD)
|
||||
if let Some(ref mut demuxer) = self.ps_demuxer {
|
||||
for ps in &demuxer.flush() {
|
||||
@@ -1021,12 +1019,13 @@ 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() {
|
||||
if let crate::disc::Stream::Video(v) = s
|
||||
&& !v.secondary
|
||||
&& self.codec_private(idx).is_none()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -436,7 +436,7 @@ impl<W: Write> M2tsMux<W> {
|
||||
}
|
||||
|
||||
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()?;
|
||||
}
|
||||
|
||||
+8
-9
@@ -1583,11 +1583,12 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
||||
// 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) {
|
||||
if keyframe
|
||||
&& is_video
|
||||
&& let Some(slot) = self.last_video_keyframe_ticks.get_mut(track_idx)
|
||||
{
|
||||
*slot = Some(pts_ticks);
|
||||
}
|
||||
}
|
||||
self.frame_count += 1;
|
||||
|
||||
// Per-track byte total for the finalize-time BPS statistics tag.
|
||||
@@ -1600,8 +1601,9 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
||||
// (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 {
|
||||
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 {
|
||||
@@ -1624,7 +1626,6 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate PGS forced-subtitle state: a display set marks the track as
|
||||
// having shown a subtitle, and clears `all_forced` the moment a
|
||||
@@ -1767,15 +1768,13 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
||||
// 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 {
|
||||
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))?;
|
||||
|
||||
self.writer.flush()?;
|
||||
|
||||
@@ -254,12 +254,12 @@ 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() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,11 +159,9 @@ fn parse_eac3(f: &[u8]) -> Option<DolbyConfig> {
|
||||
// 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,
|
||||
|
||||
+10
-7
@@ -285,11 +285,13 @@ impl<W: Write + Seek> Mp4Sink<W> {
|
||||
|
||||
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,11 +442,12 @@ impl<W: Write + Seek + Send> Stream for Mp4Sink<W> {
|
||||
// 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) {
|
||||
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;
|
||||
self.writer.write_all(&frame.data)?;
|
||||
|
||||
@@ -420,12 +420,13 @@ 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() {
|
||||
if let crate::disc::Stream::Video(v) = s
|
||||
&& !v.secondary
|
||||
&& self.codec_private(idx).is_none()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
+7
-8
@@ -193,11 +193,11 @@ pub fn parse_url(url: &str) -> StreamUrl {
|
||||
return StreamUrl::Null;
|
||||
}
|
||||
}
|
||||
if let Some(rest) = url.strip_prefix("stdio://") {
|
||||
if rest.is_empty() {
|
||||
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 {
|
||||
path: PathBuf::from(rest),
|
||||
@@ -1646,9 +1646,10 @@ 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() {
|
||||
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 {
|
||||
@@ -1660,8 +1661,6 @@ pub(crate) fn resolve_mux_key_map_cached(
|
||||
idx = pick(&samples, unit_keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// `sample_units` draws REAL content (not authored-bad units), so a sample
|
||||
// no held or fetched key decrypts to clean means this extent's CPS-unit key
|
||||
// is genuinely absent. Building a map that silently assigns a WRONG key
|
||||
|
||||
+3
-3
@@ -53,13 +53,13 @@ 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 {
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -140,8 +140,9 @@ 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) {
|
||||
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)
|
||||
@@ -149,7 +150,6 @@ impl TimelineContinuity {
|
||||
return prev_mapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+3
-3
@@ -1264,13 +1264,13 @@ 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) {
|
||||
if c != 0
|
||||
&& let Some(ch) = char::from_u32(c as u32)
|
||||
{
|
||||
s.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.trim().to_string()
|
||||
}
|
||||
_ => String::from_utf8_lossy(&content[1..])
|
||||
|
||||
Reference in New Issue
Block a user