diff --git a/CHANGELOG.md b/CHANGELOG.md index 9070e38..7f29357 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,18 @@ # Changelog +## 0.11.16 (2026-04-21) + +### API cleanup — one method per action +- **SectorReader::read_sectors(lba, count, buf, recovery)** — single method with `recovery: bool`. Removes `read_sectors_recover()`. +- **parser_for_codec(codec, codec_data)** — single constructor. Removes `parser_for_codec_with_data()`. +- **DvdSubParser::new(codec_data)** — single constructor. Removes `with_codec_data()`. +- **MkvMuxer::new(writer, tracks, title, duration, chapters)** — single constructor. Removes `new_with_chapters()`. + ## 0.11.15 (2026-04-21) ### Lint cleanup - Fix all `cargo fmt` and `cargo clippy -D warnings` across codebase. -- Remove unused imports (Codec, HdrFormat, ScanOptions, detect_max_batch_sectors, Extent). -- Fix CSS tuple pattern deref, collapsible if-statement, div_ceil reimplementation. +- Remove unused imports, dead code, collapsible if-statements, div_ceil reimplementation. ## 0.11.14 (2026-04-21) diff --git a/Cargo.toml b/Cargo.toml index 3701dc7..22fe28d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfreemkv" -version = "0.11.15" +version = "0.11.16" edition = "2021" rust-version = "1.86" license = "AGPL-3.0-only" diff --git a/src/css/mod.rs b/src/css/mod.rs index d54bbed..409f435 100644 --- a/src/css/mod.rs +++ b/src/css/mod.rs @@ -42,7 +42,11 @@ pub fn crack_key(reader: &mut dyn SectorReader, extents: &[Extent]) -> Option Result { - self.read(lba, count, buf, true) - } - - fn read_sectors_recover( + fn read_sectors( &mut self, lba: u32, count: u16, diff --git a/src/mux/codec/dvdsub.rs b/src/mux/codec/dvdsub.rs index 2d40ba4..5f85e1b 100644 --- a/src/mux/codec/dvdsub.rs +++ b/src/mux/codec/dvdsub.rs @@ -14,19 +14,8 @@ pub struct DvdSubParser { codec_data: Option>, } -impl Default for DvdSubParser { - fn default() -> Self { - Self::new() - } -} - impl DvdSubParser { - pub fn new() -> Self { - Self { codec_data: None } - } - - /// Create a parser with pre-formatted codec private data (palette header). - pub fn with_codec_data(codec_data: Option>) -> Self { + pub fn new(codec_data: Option>) -> Self { Self { codec_data } } } @@ -109,7 +98,7 @@ mod tests { #[test] fn passthrough_data() { - let mut parser = DvdSubParser::new(); + let mut parser = DvdSubParser::new(None); let sub_data = vec![0x00, 0x0A, 0x00, 0x08, 0x01, 0xFF, 0x02, 0x03, 0x04, 0x05]; let pes = make_pes(sub_data.clone(), Some(90000)); let frames = parser.parse(&pes); @@ -124,7 +113,7 @@ mod tests { #[test] fn always_keyframe() { - let mut parser = DvdSubParser::new(); + let mut parser = DvdSubParser::new(None); for i in 0..3u8 { let data = vec![0x00, i, 0x00, i + 1]; let pes = make_pes(data, Some(90000 * i as i64)); @@ -139,21 +128,21 @@ mod tests { #[test] fn empty_pes_returns_no_frames() { - let mut parser = DvdSubParser::new(); + let mut parser = DvdSubParser::new(None); let pes = make_pes(Vec::new(), Some(0)); assert!(parser.parse(&pes).is_empty()); } #[test] fn codec_private_none_by_default() { - let parser = DvdSubParser::new(); + let parser = DvdSubParser::new(None); assert!(parser.codec_private().is_none()); } #[test] fn codec_private_returns_palette_when_set() { let palette_data = b"palette: 000000, ffffff\n".to_vec(); - let parser = DvdSubParser::with_codec_data(Some(palette_data.clone())); + let parser = DvdSubParser::new(Some(palette_data.clone())); let cp = parser.codec_private(); assert!(cp.is_some()); assert_eq!(cp.unwrap(), palette_data); @@ -161,7 +150,7 @@ mod tests { #[test] fn no_pts_defaults_to_zero() { - let mut parser = DvdSubParser::new(); + let mut parser = DvdSubParser::new(None); let pes = make_pes(vec![0x01, 0x02], None); let frames = parser.parse(&pes); assert_eq!(frames.len(), 1); diff --git a/src/mux/codec/mod.rs b/src/mux/codec/mod.rs index cf8872b..cd3e0e4 100644 --- a/src/mux/codec/mod.rs +++ b/src/mux/codec/mod.rs @@ -79,18 +79,10 @@ impl CodecParser for PassthroughParser { } } -/// Create the appropriate parser for a codec. -pub fn parser_for_codec(codec: Codec) -> Box { - parser_for_codec_with_data(codec, None) -} - /// Create the appropriate parser for a codec, with optional codec private data. /// /// For DvdSub, `codec_data` should be the pre-formatted VobSub .idx palette header. -pub fn parser_for_codec_with_data( - codec: Codec, - codec_data: Option>, -) -> Box { +pub fn parser_for_codec(codec: Codec, codec_data: Option>) -> Box { match codec { Codec::H264 => Box::new(h264::H264Parser::new()), Codec::Hevc => Box::new(hevc::HevcParser::new()), @@ -101,7 +93,7 @@ pub fn parser_for_codec_with_data( Codec::TrueHd => Box::new(truehd::TrueHdParser::new()), Codec::Pgs => Box::new(pgs::PgsParser::new()), Codec::Lpcm => Box::new(lpcm::LpcmParser::new()), - Codec::DvdSub => Box::new(dvdsub::DvdSubParser::with_codec_data(codec_data)), + Codec::DvdSub => Box::new(dvdsub::DvdSubParser::new(codec_data)), _ => Box::new(PassthroughParser::new(true)), } } diff --git a/src/mux/disc.rs b/src/mux/disc.rs index 10f31ba..287c354 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -72,7 +72,7 @@ impl DiscStream { }; pids.push(pid); pid_to_track.push((pid, idx)); - parsers.push((pid, super::codec::parser_for_codec(codec))); + parsers.push((pid, super::codec::parser_for_codec(codec, None))); } let mut ts_demuxer = None; @@ -146,7 +146,7 @@ impl DiscStream { } if self .reader - .read_sectors_recover(lba, 1, &mut self.read_buf[offset..offset + 2048], false) + .read_sectors(lba, 1, &mut self.read_buf[offset..offset + 2048], false) .is_ok() { self.emit(EventKind::SectorRecovered { sector: lba as u64 }); @@ -171,7 +171,7 @@ impl DiscStream { let offset = self.buf_valid; if self .reader - .read_sectors_recover( + .read_sectors( lba, count, &mut self.read_buf[offset..offset + bytes], @@ -228,7 +228,7 @@ impl DiscStream { if self .reader - .read_sectors_recover(lba, sectors, &mut self.read_buf[..bytes], false) + .read_sectors(lba, sectors, &mut self.read_buf[..bytes], false) .is_ok() { // Fast path: batch succeeded diff --git a/src/mux/iso.rs b/src/mux/iso.rs index ebf5747..92c42b0 100644 --- a/src/mux/iso.rs +++ b/src/mux/iso.rs @@ -39,7 +39,13 @@ impl IsoSectorReader { } impl SectorReader for IsoSectorReader { - fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result { + fn read_sectors( + &mut self, + lba: u32, + count: u16, + buf: &mut [u8], + _recovery: bool, + ) -> Result { let bytes = count as usize * SECTOR_SIZE as usize; self.file .seek(SeekFrom::Start(lba as u64 * SECTOR_SIZE)) @@ -71,11 +77,11 @@ mod tests { assert_eq!(reader.capacity(), 4); let mut buf = [0u8; 2048]; - reader.read_sectors(0, 1, &mut buf).unwrap(); + reader.read_sectors(0, 1, &mut buf, true).unwrap(); assert_eq!(buf[0], 1); assert_eq!(buf[2047], 100); - reader.read_sectors(2, 1, &mut buf).unwrap(); + reader.read_sectors(2, 1, &mut buf, true).unwrap(); assert_eq!(buf[0], 3); assert_eq!(buf[2047], 102); diff --git a/src/mux/m2ts.rs b/src/mux/m2ts.rs index 7e795a8..1cf48bd 100644 --- a/src/mux/m2ts.rs +++ b/src/mux/m2ts.rs @@ -103,7 +103,7 @@ impl M2tsStream { }; pids.push(pid); pid_to_track.push((pid, i)); - parsers.push((pid, super::codec::parser_for_codec(codec))); + parsers.push((pid, super::codec::parser_for_codec(codec, None))); } (pids, parsers, pid_to_track) } diff --git a/src/mux/mkv.rs b/src/mux/mkv.rs index caf7361..a19d903 100644 --- a/src/mux/mkv.rs +++ b/src/mux/mkv.rs @@ -182,16 +182,6 @@ const CLUSTER_DURATION_MS: i64 = 5000; impl MkvMuxer { /// Create a new MKV muxer: writes EBML header, Segment start, Info, Tracks, Chapters. pub fn new( - writer: W, - tracks: &[MkvTrack], - title: Option<&str>, - duration_secs: f64, - ) -> io::Result { - Self::new_with_chapters(writer, tracks, title, duration_secs, &[]) - } - - /// Create a new MKV muxer with chapters: writes EBML header, Segment start, Info, Tracks, Chapters. - pub fn new_with_chapters( mut writer: W, tracks: &[MkvTrack], title: Option<&str>, @@ -580,7 +570,7 @@ mod tests { fn mkv_writes_ebml_header() { let buf = Cursor::new(Vec::new()); let tracks = [make_video_track()]; - let muxer = MkvMuxer::new(buf, &tracks, Some("Test"), 120.0).unwrap(); + let muxer = MkvMuxer::new(buf, &tracks, Some("Test"), 120.0, &[]).unwrap(); let data = muxer.writer.into_inner(); // EBML header element ID: 0x1A45DFA3 assert!(data.len() >= 4); @@ -591,7 +581,7 @@ mod tests { fn mkv_writes_segment() { let buf = Cursor::new(Vec::new()); let tracks = [make_video_track()]; - let muxer = MkvMuxer::new(buf, &tracks, None, 0.0).unwrap(); + let muxer = MkvMuxer::new(buf, &tracks, None, 0.0, &[]).unwrap(); let data = muxer.writer.into_inner(); // Segment element ID: 0x18538067 assert!( @@ -604,7 +594,7 @@ mod tests { fn mkv_write_frame_creates_cluster() { let buf = Cursor::new(Vec::new()); let tracks = [make_video_track()]; - let mut muxer = MkvMuxer::new(buf, &tracks, None, 60.0).unwrap(); + let mut muxer = MkvMuxer::new(buf, &tracks, None, 60.0, &[]).unwrap(); muxer .write_frame(0, 0, true, &[0xDE, 0xAD, 0xBE, 0xEF]) .unwrap(); @@ -643,7 +633,7 @@ mod tests { let shared = Arc::new(Mutex::new(Cursor::new(Vec::new()))); let writer = SharedWriter(shared.clone()); let tracks = [make_video_track()]; - let mut muxer = MkvMuxer::new(writer, &tracks, Some("Cue Test"), 60.0).unwrap(); + let mut muxer = MkvMuxer::new(writer, &tracks, Some("Cue Test"), 60.0, &[]).unwrap(); muxer.write_frame(0, 0, true, &[0x01, 0x02, 0x03]).unwrap(); muxer.finish().unwrap(); @@ -658,7 +648,7 @@ mod tests { fn mkv_multiple_tracks() { let buf = Cursor::new(Vec::new()); let tracks = [make_video_track(), make_audio_track()]; - let mut muxer = MkvMuxer::new(buf, &tracks, Some("Multi"), 120.0).unwrap(); + let mut muxer = MkvMuxer::new(buf, &tracks, Some("Multi"), 120.0, &[]).unwrap(); // Write frames to both tracks muxer.write_frame(0, 0, true, &[0x00, 0x00, 0x01]).unwrap(); muxer.write_frame(1, 0, false, &[0x0B, 0x77, 0x00]).unwrap(); @@ -677,7 +667,7 @@ mod tests { fn mkv_keyframe_flag() { let buf = Cursor::new(Vec::new()); let tracks = [make_video_track()]; - let mut muxer = MkvMuxer::new(buf, &tracks, None, 10.0).unwrap(); + let mut muxer = MkvMuxer::new(buf, &tracks, None, 10.0, &[]).unwrap(); // Record position before first frame let pos_before_kf = muxer.writer.position(); @@ -751,9 +741,7 @@ mod tests { name: "Chapter 3".into(), }, ]; - let muxer = - MkvMuxer::new_with_chapters(buf, &tracks, Some("Chapter Test"), 900.0, &chapters) - .unwrap(); + let muxer = MkvMuxer::new(buf, &tracks, Some("Chapter Test"), 900.0, &chapters).unwrap(); let data = muxer.writer.into_inner(); // Chapters element ID: 0x1043A770 @@ -777,7 +765,7 @@ mod tests { fn mkv_no_chapters_when_empty() { let buf = Cursor::new(Vec::new()); let tracks = [make_video_track()]; - let muxer = MkvMuxer::new(buf, &tracks, Some("No Chapters"), 60.0).unwrap(); + let muxer = MkvMuxer::new(buf, &tracks, Some("No Chapters"), 60.0, &[]).unwrap(); let data = muxer.writer.into_inner(); assert!( find_id(&data, ebml::CHAPTERS).is_none(), @@ -796,7 +784,7 @@ mod tests { let buf = Cursor::new(Vec::new()); let tracks = [video, audio1, audio2]; - let muxer = MkvMuxer::new(buf, &tracks, None, 60.0).unwrap(); + let muxer = MkvMuxer::new(buf, &tracks, None, 60.0, &[]).unwrap(); let data = muxer.writer.into_inner(); // FlagDefault ID is 0x88. When is_default is true, FlagDefault is NOT written @@ -828,7 +816,7 @@ mod tests { let buf = Cursor::new(Vec::new()); let tracks = [video, forced_sub]; - let muxer = MkvMuxer::new(buf, &tracks, None, 60.0).unwrap(); + let muxer = MkvMuxer::new(buf, &tracks, None, 60.0, &[]).unwrap(); let data = muxer.writer.into_inner(); // FlagForced ID: 0x55AA (2-byte ID) @@ -853,7 +841,7 @@ mod tests { let buf = Cursor::new(Vec::new()); let tracks = [video, sub]; - let muxer = MkvMuxer::new(buf, &tracks, None, 60.0).unwrap(); + let muxer = MkvMuxer::new(buf, &tracks, None, 60.0, &[]).unwrap(); let data = muxer.writer.into_inner(); // FlagForced should NOT be written for non-forced tracks diff --git a/src/mux/mkvstream.rs b/src/mux/mkvstream.rs index d5718eb..502a352 100644 --- a/src/mux/mkvstream.rs +++ b/src/mux/mkvstream.rs @@ -64,7 +64,7 @@ impl MkvStream { tracks.push(track); } - let muxer = MkvMuxer::new_with_chapters( + let muxer = MkvMuxer::new( writer, &tracks, Some(&title.playlist), diff --git a/src/mux/tsreader.rs b/src/mux/tsreader.rs index 236378b..8c5132a 100644 --- a/src/mux/tsreader.rs +++ b/src/mux/tsreader.rs @@ -37,7 +37,7 @@ impl TsDemuxReader { }; pids.push(pid); pid_to_track.push((pid, i)); - parsers.push((pid, codec::parser_for_codec(c))); + parsers.push((pid, codec::parser_for_codec(c, None))); } Self { diff --git a/src/sector.rs b/src/sector.rs index d024a71..0b8b5bf 100644 --- a/src/sector.rs +++ b/src/sector.rs @@ -10,23 +10,15 @@ use crate::error::Result; pub trait SectorReader: Send { /// Read `count` sectors starting at `lba` into `buf`. /// `buf` must be at least `count * 2048` bytes. - /// Full recovery enabled (retry + reset on failure). - fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result; - - /// Read with explicit recovery flag. - /// true = full retry/reset loop (for ripping). false = single attempt, fast fail (for verify). - /// Default: delegates to read_sectors (recovery=true behavior). - fn read_sectors_recover( + /// `recovery`: true = full retry/reset loop (ripping), false = single attempt (verify). + /// File-backed readers ignore the flag. + fn read_sectors( &mut self, lba: u32, count: u16, buf: &mut [u8], recovery: bool, - ) -> Result { - // Default ignores flag — file-backed readers don't have recovery - let _ = recovery; - self.read_sectors(lba, count, buf) - } + ) -> Result; /// Total capacity in sectors, if known. fn capacity(&self) -> u32 { @@ -61,7 +53,13 @@ impl FileSectorReader { } impl SectorReader for FileSectorReader { - fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result { + fn read_sectors( + &mut self, + lba: u32, + count: u16, + buf: &mut [u8], + _recovery: bool, + ) -> Result { use std::io::{Read, Seek, SeekFrom}; let offset = lba as u64 * 2048; let bytes = count as usize * 2048; diff --git a/src/udf.rs b/src/udf.rs index a263070..b431789 100644 --- a/src/udf.rs +++ b/src/udf.rs @@ -879,6 +879,7 @@ impl BufferedSectorReader<'_> { start_lba + offset, batch, &mut self.cache[buf_off..buf_off + batch as usize * 2048], + true, ) .is_err() { @@ -902,7 +903,7 @@ impl BufferedSectorReader<'_> { let bytes = batch as usize * 2048; if self .inner - .read_sectors(start + offset, batch, &mut tmp[..bytes]) + .read_sectors(start + offset, batch, &mut tmp[..bytes], true) .is_err() { break; @@ -924,6 +925,7 @@ impl SectorReader for BufferedSectorReader<'_> { lba: u32, count: u16, buf: &mut [u8], + _recovery: bool, ) -> std::result::Result { if count == 1 { // Check permanent prefetch cache first (HashMap) @@ -939,7 +941,7 @@ impl SectorReader for BufferedSectorReader<'_> { } let block = self.batch; self.cache.resize(block as usize * 2048, 0); - match self.inner.read_sectors(lba, block, &mut self.cache) { + match self.inner.read_sectors(lba, block, &mut self.cache, true) { Ok(_) => { self.cache_start = lba; self.cache_sectors = block as u32; @@ -947,7 +949,7 @@ impl SectorReader for BufferedSectorReader<'_> { Err(_) => { // Near end of disc or error — single sector fallback self.cache.resize(2048, 0); - self.inner.read_sectors(lba, 1, &mut self.cache)?; + self.inner.read_sectors(lba, 1, &mut self.cache, true)?; self.cache_start = lba; self.cache_sectors = 1; } @@ -956,12 +958,12 @@ impl SectorReader for BufferedSectorReader<'_> { Ok(2048) } else { // Multi-sector read — pass through - self.inner.read_sectors(lba, count, buf) + self.inner.read_sectors(lba, count, buf, true) } } } fn read_sector(reader: &mut dyn SectorReader, lba: u32, buf: &mut [u8]) -> Result<()> { - reader.read_sectors(lba, 1, buf)?; + reader.read_sectors(lba, 1, buf, true)?; Ok(()) } diff --git a/src/verify.rs b/src/verify.rs index 2081406..eab4c52 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -113,7 +113,7 @@ pub fn verify_title( let batch_start = Instant::now(); let batch_ok = reader - .read_sectors_recover(lba, count, &mut buf[..bytes], false) + .read_sectors(lba, count, &mut buf[..bytes], false) .is_ok(); let batch_ms = batch_start.elapsed().as_millis(); @@ -155,7 +155,7 @@ pub fn verify_title( let s1 = Instant::now(); let first_ok = reader - .read_sectors_recover( + .read_sectors( sector_lba, 1, &mut buf[sector_offset..sector_offset + 2048], @@ -174,7 +174,7 @@ pub fn verify_title( // Retry once more after brief pause std::thread::sleep(std::time::Duration::from_secs(2)); if reader - .read_sectors_recover( + .read_sectors( sector_lba, 1, &mut buf[sector_offset..sector_offset + 2048], diff --git a/tests/disc_tests.rs b/tests/disc_tests.rs index b6ac4d6..2a0fe79 100644 --- a/tests/disc_tests.rs +++ b/tests/disc_tests.rs @@ -21,7 +21,7 @@ impl MockSectorReader { } impl SectorReader for MockSectorReader { - fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result { + fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8], _recovery: bool) -> Result { let total = count as usize * SECTOR_SIZE; for i in 0..count as u32 { let offset = i as usize * SECTOR_SIZE; diff --git a/tests/udf_tests.rs b/tests/udf_tests.rs index be4d3c0..71b424a 100644 --- a/tests/udf_tests.rs +++ b/tests/udf_tests.rs @@ -39,7 +39,7 @@ impl MockSectorReader { } impl SectorReader for MockSectorReader { - fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result { + fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8], _recovery: bool) -> Result { let total = count as usize * SECTOR_SIZE; assert!(buf.len() >= total, "buffer too small"); for i in 0..count as u32 { @@ -215,7 +215,7 @@ fn mock_sector_reader_roundtrip() { // Read it back let mut buf = vec![0u8; SECTOR_SIZE]; - let n = reader.read_sectors(100, 1, &mut buf).unwrap(); + let n = reader.read_sectors(100, 1, &mut buf, true).unwrap(); assert_eq!(n, SECTOR_SIZE); assert_eq!(buf[0], 0xAB); assert_eq!(buf[1], 0xCD); @@ -223,7 +223,7 @@ fn mock_sector_reader_roundtrip() { // Reading an unmapped sector returns zeros let mut buf2 = vec![0xFFu8; SECTOR_SIZE]; - let n2 = reader.read_sectors(999, 1, &mut buf2).unwrap(); + let n2 = reader.read_sectors(999, 1, &mut buf2, true).unwrap(); assert_eq!(n2, SECTOR_SIZE); assert_eq!(buf2[0], 0); assert_eq!(buf2[2047], 0); @@ -243,7 +243,7 @@ fn mock_sector_reader_multi_sector() { // Read 2 consecutive sectors let mut buf = vec![0u8; SECTOR_SIZE * 2]; - let n = reader.read_sectors(10, 2, &mut buf).unwrap(); + let n = reader.read_sectors(10, 2, &mut buf, true).unwrap(); assert_eq!(n, SECTOR_SIZE * 2); assert_eq!(buf[0], 10); assert_eq!(buf[SECTOR_SIZE], 11); @@ -463,7 +463,7 @@ fn sector_reader_is_object_safe() { let dyn_reader: &mut dyn SectorReader = &mut reader; let mut buf = vec![0u8; SECTOR_SIZE]; - let n = dyn_reader.read_sectors(0, 1, &mut buf).unwrap(); + let n = dyn_reader.read_sectors(0, 1, &mut buf, true).unwrap(); assert_eq!(n, SECTOR_SIZE); assert_eq!(buf[0], 42); }