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:
+131
-17
@@ -689,7 +689,15 @@ impl Disc {
|
|||||||
pkt_count = clip_info.source_packet_count;
|
pkt_count = clip_info.source_packet_count;
|
||||||
total_size += pkt_count as u64 * 192;
|
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);
|
extents.extend(clip_extents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -779,8 +787,17 @@ pub struct ContentReader<'a> {
|
|||||||
aacs: Option<&'a AacsState>,
|
aacs: Option<&'a AacsState>,
|
||||||
extents: Vec<Extent>,
|
extents: Vec<Extent>,
|
||||||
current_extent: usize,
|
current_extent: usize,
|
||||||
current_offset: u32, // sectors into current extent
|
current_offset: u32,
|
||||||
unit_key_idx: usize,
|
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 {
|
impl Disc {
|
||||||
@@ -804,6 +821,11 @@ impl Disc {
|
|||||||
detail: format!("title index {} out of range (have {})", title_idx, self.titles.len()),
|
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 {
|
Ok(ContentReader {
|
||||||
session,
|
session,
|
||||||
aacs: self.aacs.as_ref(),
|
aacs: self.aacs.as_ref(),
|
||||||
@@ -811,31 +833,38 @@ impl Disc {
|
|||||||
current_extent: 0,
|
current_extent: 0,
|
||||||
current_offset: 0,
|
current_offset: 0,
|
||||||
unit_key_idx: 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> {
|
impl<'a> ContentReader<'a> {
|
||||||
/// Read the next aligned unit (6144 bytes).
|
/// Read the next aligned unit (6144 bytes).
|
||||||
/// Automatically decrypted if AACS keys are available.
|
/// Automatically decrypted if AACS keys are available.
|
||||||
/// Returns None when all extents are exhausted.
|
/// Returns None when all extents are exhausted.
|
||||||
pub fn read_unit(&mut self) -> Result<Option<Vec<u8>>> {
|
pub fn read_unit(&mut self) -> Result<Option<Vec<u8>>> {
|
||||||
if self.current_extent >= self.extents.len() {
|
// Refill buffer if empty
|
||||||
|
if self.buf_pos >= self.buf_len {
|
||||||
|
if !self.fill_buffer()? {
|
||||||
return Ok(None);
|
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(§or);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Decrypt if needed
|
||||||
if let Some(aacs) = &self.aacs {
|
if let Some(aacs) = &self.aacs {
|
||||||
if crate::aacs::is_unit_encrypted(&unit) {
|
if crate::aacs::is_unit_encrypted(&unit) {
|
||||||
@@ -851,14 +880,99 @@ impl<'a> ContentReader<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance position
|
self.buf_pos += 1;
|
||||||
self.current_offset += 3;
|
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 {
|
if self.current_offset >= extent.sector_count {
|
||||||
self.current_extent += 1;
|
self.current_extent += 1;
|
||||||
self.current_offset = 0;
|
self.current_offset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(unit))
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -149,7 +149,7 @@ impl DriveSession {
|
|||||||
self.platform.probe(self.scsi.as_mut(), sub_cmd, address, length)
|
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> {
|
pub fn read_disc(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||||
let cdb = [
|
let cdb = [
|
||||||
crate::scsi::SCSI_READ_10, 0x00,
|
crate::scsi::SCSI_READ_10, 0x00,
|
||||||
@@ -163,6 +163,20 @@ impl DriveSession {
|
|||||||
Ok(result.bytes_transferred)
|
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.
|
/// Eject the disc tray.
|
||||||
///
|
///
|
||||||
/// Sends PREVENT ALLOW MEDIUM REMOVAL (allow) first to release any
|
/// Sends PREVENT ALLOW MEDIUM REMOVAL (allow) first to release any
|
||||||
|
|||||||
+22
@@ -72,6 +72,28 @@ impl UdfFs {
|
|||||||
|
|
||||||
/// Read a file by path, returning its raw bytes.
|
/// Read a file by path, returning its raw bytes.
|
||||||
/// Reads sector by sector from disc — no buffering.
|
/// 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>> {
|
pub fn read_file(&self, session: &mut DriveSession, path: &str) -> Result<Vec<u8>> {
|
||||||
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
|
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
|
||||||
let mut current = &self.root;
|
let mut current = &self.root;
|
||||||
|
|||||||
Reference in New Issue
Block a user