Fix rip: extent LBA offset, u16 truncation, batch reads, read_content

- Fixed: extents were relative to m2ts file, not absolute disc LBAs
- Fixed: u16 truncation of remaining sector count (13M → 36!)
- Added: UdfFs::file_start_lba() for m2ts LBA lookup
- Added: DriveSession::read_content() with 30s timeout for bulk reads
- Added: SET CD SPEED 0xFFFF on title open
- Added: adaptive batch reading (96→48→3 on error, ramp back up)
- Rip working end-to-end: scan → AACS → decrypt → write
This commit is contained in:
MattJackson
2026-04-08 10:12:07 -07:00
parent 2d3287da82
commit 1e975d450a
3 changed files with 173 additions and 23 deletions
+136 -22
View File
@@ -689,7 +689,15 @@ impl Disc {
pkt_count = clip_info.source_packet_count;
total_size += pkt_count as u64 * 192;
let clip_extents = clip_info.get_extents(play_item.in_time, play_item.out_time);
// Get the m2ts file's absolute starting LBA on disc
let m2ts_path = format!("/BDMV/STREAM/{}.m2ts", play_item.clip_id);
let file_lba = udf_fs.file_start_lba(session, &m2ts_path).unwrap_or(0);
let mut clip_extents = clip_info.get_extents(play_item.in_time, play_item.out_time);
// Extents from CLPI are relative to m2ts file start — add file LBA
for ext in &mut clip_extents {
ext.start_lba += file_lba;
}
extents.extend(clip_extents);
}
}
@@ -779,8 +787,17 @@ pub struct ContentReader<'a> {
aacs: Option<&'a AacsState>,
extents: Vec<Extent>,
current_extent: usize,
current_offset: u32, // sectors into current extent
current_offset: u32,
unit_key_idx: usize,
read_buf: Vec<u8>,
buf_pos: usize,
buf_len: usize,
/// Current batch size (adapts on errors)
batch_sectors: u16,
/// Consecutive successful batch reads (for ramp-up)
ok_streak: u32,
/// Total read errors encountered
pub errors: u32,
}
impl Disc {
@@ -804,6 +821,11 @@ impl Disc {
detail: format!("title index {} out of range (have {})", title_idx, self.titles.len()),
})?;
// Set drive to max read speed
let speed_cdb = crate::scsi::build_set_cd_speed(0xFFFF);
let mut dummy = [0u8; 0];
let _ = session.scsi_execute(&speed_cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000);
Ok(ContentReader {
session,
aacs: self.aacs.as_ref(),
@@ -811,30 +833,37 @@ impl Disc {
current_extent: 0,
current_offset: 0,
unit_key_idx: 0,
read_buf: Vec::with_capacity(MAX_BATCH_SECTORS as usize * 2048),
buf_pos: 0,
buf_len: 0,
batch_sectors: MAX_BATCH_SECTORS,
ok_streak: 0,
errors: 0,
})
}
}
/// Read strategy constants
const MAX_BATCH_SECTORS: u16 = 96; // 32 aligned units = 192KB per command (fast)
const MIN_BATCH_SECTORS: u16 = 3; // 1 aligned unit = 6KB (slow, for error recovery)
const RAMP_UP_AFTER: u32 = 10; // successful reads before ramping back up
impl<'a> ContentReader<'a> {
/// Read the next aligned unit (6144 bytes).
/// Automatically decrypted if AACS keys are available.
/// Returns None when all extents are exhausted.
pub fn read_unit(&mut self) -> Result<Option<Vec<u8>>> {
if self.current_extent >= self.extents.len() {
return Ok(None);
// Refill buffer if empty
if self.buf_pos >= self.buf_len {
if !self.fill_buffer()? {
return Ok(None);
}
}
let extent = &self.extents[self.current_extent];
let lba = extent.start_lba + self.current_offset;
// Read 3 sectors (one aligned unit)
let mut unit = vec![0u8; crate::aacs::ALIGNED_UNIT_LEN];
for i in 0..3u32 {
let offset = (i as usize) * 2048;
let mut sector = [0u8; 2048];
session_read_sector(self.session, lba + i, &mut sector)?;
unit[offset..offset + 2048].copy_from_slice(&sector);
}
// Extract one aligned unit from buffer
let start = self.buf_pos * crate::aacs::ALIGNED_UNIT_LEN;
let end = start + crate::aacs::ALIGNED_UNIT_LEN;
let mut unit = self.read_buf[start..end].to_vec();
// Decrypt if needed
if let Some(aacs) = &self.aacs {
@@ -851,15 +880,100 @@ impl<'a> ContentReader<'a> {
}
}
// Advance position
self.current_offset += 3;
if self.current_offset >= extent.sector_count {
self.current_extent += 1;
self.current_offset = 0;
}
self.buf_pos += 1;
Ok(Some(unit))
}
/// Read a batch of sectors into the internal buffer.
/// Adapts batch size on errors: shrinks on failure, grows on success.
fn fill_buffer(&mut self) -> Result<bool> {
loop {
if self.current_extent >= self.extents.len() {
eprintln!(" [done] extent {}/{} offset {} errors {}",
self.current_extent, self.extents.len(), self.current_offset, self.errors);
return Ok(false);
}
let extent = &self.extents[self.current_extent];
let remaining = extent.sector_count - self.current_offset;
// Align to 3 sectors (one aligned unit)
let sectors_to_read = remaining.min(self.batch_sectors as u32) as u16;
let sectors_to_read = sectors_to_read - (sectors_to_read % 3);
if sectors_to_read == 0 {
self.current_extent += 1;
self.current_offset = 0;
continue;
}
let lba = extent.start_lba + self.current_offset;
let byte_count = sectors_to_read as usize * 2048;
self.read_buf.resize(byte_count, 0);
if self.current_offset < 100 || sectors_to_read < self.batch_sectors {
eprintln!(" [fill] lba={} count={} offset={}/{} batch={} err={} remaining={}",
lba, sectors_to_read, self.current_offset, extent.sector_count,
self.batch_sectors, self.errors, remaining);
}
match self.session.read_content(lba, sectors_to_read, &mut self.read_buf) {
Ok(_) => {
self.buf_len = sectors_to_read as usize / 3;
self.buf_pos = 0;
self.current_offset += sectors_to_read as u32;
if self.current_offset >= extent.sector_count {
self.current_extent += 1;
self.current_offset = 0;
}
// Ramp up batch size after consecutive successes
self.ok_streak += 1;
if self.ok_streak >= RAMP_UP_AFTER && self.batch_sectors < MAX_BATCH_SECTORS {
self.batch_sectors = (self.batch_sectors * 2).min(MAX_BATCH_SECTORS);
self.ok_streak = 0;
}
return Ok(true);
}
Err(_) => {
self.errors += 1;
self.ok_streak = 0;
if self.batch_sectors > MIN_BATCH_SECTORS {
// Shrink batch and retry
self.batch_sectors = (self.batch_sectors / 2).max(MIN_BATCH_SECTORS);
// Brief pause to let drive recover
std::thread::sleep(std::time::Duration::from_millis(100));
} else {
// At minimum batch — retry once with a longer pause
std::thread::sleep(std::time::Duration::from_millis(500));
self.read_buf.resize(MIN_BATCH_SECTORS as usize * 2048, 0);
if self.session.read_content(lba, MIN_BATCH_SECTORS, &mut self.read_buf).is_ok() {
self.buf_len = 1;
self.buf_pos = 0;
self.current_offset += MIN_BATCH_SECTORS as u32;
if self.current_offset >= extent.sector_count {
self.current_extent += 1;
self.current_offset = 0;
}
return Ok(true);
}
// Still failing — skip this unit
self.current_offset += 3;
if self.current_offset >= extent.sector_count {
self.current_extent += 1;
self.current_offset = 0;
}
self.read_buf.resize(crate::aacs::ALIGNED_UNIT_LEN, 0);
self.read_buf.fill(0);
self.buf_len = 1;
self.buf_pos = 0;
return Ok(true);
}
}
}
}
}
}
fn session_read_sector(session: &mut DriveSession, lba: u32, buf: &mut [u8; 2048]) -> Result<()> {
+15 -1
View File
@@ -149,7 +149,7 @@ impl DriveSession {
self.platform.probe(self.scsi.as_mut(), sub_cmd, address, length)
}
/// Standard SCSI READ(10) for disc filesystem data (UDF, MPLS, CLPI).
/// SCSI READ(10) for disc filesystem data (UDF, MPLS, CLPI).
pub fn read_disc(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
let cdb = [
crate::scsi::SCSI_READ_10, 0x00,
@@ -163,6 +163,20 @@ impl DriveSession {
Ok(result.bytes_transferred)
}
/// SCSI READ(10) for m2ts content — bulk reads with longer timeout.
pub fn read_content(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
let cdb = [
crate::scsi::SCSI_READ_10, 0x00,
(lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8,
0x00,
(count >> 8) as u8, count as u8,
0x00,
];
let result = self.scsi.as_mut().execute(
&cdb, crate::scsi::DataDirection::FromDevice, buf, 30_000)?;
Ok(result.bytes_transferred)
}
/// Eject the disc tray.
///
/// Sends PREVENT ALLOW MEDIUM REMOVAL (allow) first to release any
+22
View File
@@ -72,6 +72,28 @@ impl UdfFs {
/// Read a file by path, returning its raw bytes.
/// Reads sector by sector from disc — no buffering.
/// Get the absolute starting LBA of a file on disc.
/// Used by the rip pipeline to locate m2ts content sectors.
pub fn file_start_lba(&self, session: &mut DriveSession, path: &str) -> Result<u32> {
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
let mut current = &self.root;
for part in &parts[..parts.len() - 1] {
current = current.entries.iter().find(|e| {
e.is_dir && e.name.eq_ignore_ascii_case(part)
}).ok_or_else(|| Error::DiscError {
detail: format!("directory not found: {}", part),
})?;
}
let filename = parts.last().unwrap();
let entry = current.entries.iter().find(|e| {
!e.is_dir && e.name.eq_ignore_ascii_case(filename)
}).ok_or_else(|| Error::DiscError {
detail: format!("file not found: {}", path),
})?;
let (data_lba, _) = self.read_icb_extent(session, entry.meta_lba)?;
Ok(self.partition_start + data_lba)
}
pub fn read_file(&self, session: &mut DriveSession, path: &str) -> Result<Vec<u8>> {
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
let mut current = &self.root;