Strip to bare minimum for speed test: no calibration, no maintain_speed
Back to basics: open, unlock, SET CD SPEED max, read. Remove all calibration probes, register reads, maintain_speed calls. This is closest to the build that hit 17 MB/s earlier. Also: drive discovery moved to libfreemkv (find_drive, resolve_device), AACS via UDF only, clean pipeline, sg device support.
This commit is contained in:
+230
-145
@@ -415,14 +415,39 @@ impl Disc {
|
|||||||
/// println!("{} — {} streams", title.duration_display(), title.streams.len());
|
/// println!("{} — {} streams", title.duration_display(), title.streams.len());
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
/// Scan a disc. One pipeline, one order:
|
||||||
|
/// 1. Read capacity
|
||||||
|
/// 2. Read UDF filesystem
|
||||||
|
/// 3. Resolve AACS keys (all via UDF, no SCSI commands)
|
||||||
|
/// 4. Parse playlists + streams
|
||||||
|
/// 5. Apply labels
|
||||||
|
///
|
||||||
|
/// The session must be open and unlocked (DriveSession::open handles this).
|
||||||
|
/// All disc reads use standard READ(10) via UDF — no vendor SCSI commands.
|
||||||
pub fn scan(session: &mut DriveSession, opts: &ScanOptions) -> Result<Self> {
|
pub fn scan(session: &mut DriveSession, opts: &ScanOptions) -> Result<Self> {
|
||||||
// Step 1: Read capacity
|
use crate::aacs::{self, KeyDb};
|
||||||
|
|
||||||
|
// 1. Capacity
|
||||||
let capacity = Self::read_capacity(session)?;
|
let capacity = Self::read_capacity(session)?;
|
||||||
|
|
||||||
// Step 2: Parse UDF filesystem
|
// 2. UDF filesystem
|
||||||
let udf_fs = udf::read_filesystem(session)?;
|
let udf_fs = udf::read_filesystem(session)?;
|
||||||
|
|
||||||
// Step 3: Find and parse MPLS playlists
|
// 3. AACS — read files from disc via UDF, resolve keys via KEYDB
|
||||||
|
let encrypted = udf_fs.find_dir("/AACS").is_some()
|
||||||
|
|| udf_fs.find_dir("/BDMV/AACS").is_some();
|
||||||
|
|
||||||
|
let aacs = if encrypted {
|
||||||
|
if let Some(keydb_path) = opts.resolve_keydb() {
|
||||||
|
Self::resolve_aacs(&udf_fs, session, &keydb_path).ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Playlists
|
||||||
let mut titles = Vec::new();
|
let mut titles = Vec::new();
|
||||||
if let Some(playlist_dir) = udf_fs.find_dir("/BDMV/PLAYLIST") {
|
if let Some(playlist_dir) = udf_fs.find_dir("/BDMV/PLAYLIST") {
|
||||||
for entry in &playlist_dir.entries {
|
for entry in &playlist_dir.entries {
|
||||||
@@ -436,53 +461,20 @@ impl Disc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort: longest first
|
|
||||||
titles.sort_by(|a, b| b.duration_secs.partial_cmp(&a.duration_secs).unwrap_or(std::cmp::Ordering::Equal));
|
titles.sort_by(|a, b| b.duration_secs.partial_cmp(&a.duration_secs).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
|
||||||
// Step 4: Read disc title from META/DL/bdmt_eng.xml
|
// 5. Metadata + labels
|
||||||
let meta_title = Self::read_meta_title(session, &udf_fs);
|
let meta_title = Self::read_meta_title(session, &udf_fs);
|
||||||
|
|
||||||
// Step 5: Enhance streams with disc config file labels (if available)
|
|
||||||
crate::labels::apply(session, &udf_fs, &mut titles);
|
crate::labels::apply(session, &udf_fs, &mut titles);
|
||||||
|
|
||||||
// Step 6: Detect AACS encryption
|
// 6. Derive format, layers, region
|
||||||
let encrypted = udf_fs.find_dir("/AACS").is_some()
|
|
||||||
|| udf_fs.find_dir("/BDMV/AACS").is_some();
|
|
||||||
|
|
||||||
// Step 7: If encrypted and KEYDB available, authenticate and derive keys
|
|
||||||
let aacs = if encrypted {
|
|
||||||
if let Some(keydb_path) = opts.resolve_keydb() {
|
|
||||||
match Self::setup_aacs(session, &keydb_path) {
|
|
||||||
Ok(state) => Some(state),
|
|
||||||
Err(_) => None, // keys not found, continue without decryption
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Derive disc format from main title video codec
|
|
||||||
let format = Self::detect_format(&titles);
|
let format = Self::detect_format(&titles);
|
||||||
|
|
||||||
// Derive layer count from capacity
|
|
||||||
// BD-25 single layer: up to ~12M sectors (~25GB)
|
|
||||||
// BD-50 dual layer: ~12M-25M sectors (~50GB)
|
|
||||||
// BD-66/100 UHD: 25M+ sectors
|
|
||||||
let layers = if capacity > 24_000_000 { 2 } else { 1 };
|
let layers = if capacity > 24_000_000 { 2 } else { 1 };
|
||||||
|
let region = if format == DiscFormat::Uhd { DiscRegion::Free } else { DiscRegion::Free };
|
||||||
// UHD is always region-free. BD/DVD region parsing TODO.
|
|
||||||
let region = if format == DiscFormat::Uhd {
|
|
||||||
DiscRegion::Free
|
|
||||||
} else {
|
|
||||||
DiscRegion::Free // TODO: parse from index.bdmv
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Disc {
|
Ok(Disc {
|
||||||
volume_id: udf_fs.volume_id.clone(),
|
volume_id: udf_fs.volume_id.clone(),
|
||||||
meta_title: meta_title,
|
meta_title,
|
||||||
format,
|
format,
|
||||||
capacity_sectors: capacity,
|
capacity_sectors: capacity,
|
||||||
capacity_bytes: capacity as u64 * 2048,
|
capacity_bytes: capacity as u64 * 2048,
|
||||||
@@ -494,98 +486,63 @@ impl Disc {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set up AACS decryption for this disc.
|
/// Resolve AACS keys from disc files + KEYDB. No SCSI commands.
|
||||||
/// Call after scan() to enable transparent content decryption.
|
/// Reads Unit_Key_RO.inf, Content Certificate, and MKB from UDF.
|
||||||
pub fn setup_aacs(
|
fn resolve_aacs(
|
||||||
|
udf_fs: &udf::UdfFs,
|
||||||
session: &mut DriveSession,
|
session: &mut DriveSession,
|
||||||
keydb_path: &std::path::Path,
|
keydb_path: &std::path::Path,
|
||||||
) -> Result<AacsState> {
|
) -> Result<AacsState> {
|
||||||
use crate::aacs::{self, KeyDb};
|
use crate::aacs::{self, KeyDb};
|
||||||
use crate::aacs::handshake;
|
|
||||||
|
|
||||||
// Load KEYDB
|
|
||||||
let keydb = KeyDb::load(keydb_path).map_err(|e| Error::AacsError {
|
let keydb = KeyDb::load(keydb_path).map_err(|e| Error::AacsError {
|
||||||
detail: format!("failed to load KEYDB: {}", e),
|
detail: format!("failed to load KEYDB: {}", e),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Step 1: Try SCSI handshake for Volume ID + read_data_key
|
// Read AACS files from disc via UDF (standard READ(10), no vendor commands)
|
||||||
// Open a separate transport (AACS auth must happen before raw mode).
|
|
||||||
// If handshake fails (drive doesn't support AACS layer, e.g. raw-mode drives),
|
|
||||||
// fall back to disc-hash-only KEYDB lookup.
|
|
||||||
let device_path = session.device_path().to_string();
|
|
||||||
let mut vid: Option<[u8; 16]> = None;
|
|
||||||
let mut read_data_key: Option<[u8; 16]> = None;
|
|
||||||
|
|
||||||
if !device_path.is_empty() {
|
|
||||||
if let Ok(mut aacs_session) = DriveSession::open_no_unlock(std::path::Path::new(&device_path)) {
|
|
||||||
if let Ok(hc) = keydb.host_cert.as_ref().ok_or(()) {
|
|
||||||
if let Ok(mut auth) = handshake::aacs2_authenticate(
|
|
||||||
&mut aacs_session,
|
|
||||||
&hc.private_key,
|
|
||||||
&hc.certificate,
|
|
||||||
hc.private_key_v2.as_ref(),
|
|
||||||
hc.certificate_v2.as_deref(),
|
|
||||||
) {
|
|
||||||
vid = handshake::read_volume_id(&mut aacs_session, &mut auth).ok();
|
|
||||||
read_data_key = handshake::read_data_keys(&mut aacs_session, &mut auth)
|
|
||||||
.ok().map(|(rdk, _)| rdk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Handshake failure is not fatal — we can still resolve via disc hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Read Unit_Key_RO.inf from disc via UDF (uses the unlocked main session)
|
|
||||||
let udf_fs = udf::read_filesystem(session)?;
|
|
||||||
let uk_ro_data = udf_fs.read_file(session, "/AACS/Unit_Key_RO.inf")
|
let uk_ro_data = udf_fs.read_file(session, "/AACS/Unit_Key_RO.inf")
|
||||||
.or_else(|_| udf_fs.read_file(session, "/AACS/DUPLICATE/Unit_Key_RO.inf"))
|
.or_else(|_| udf_fs.read_file(session, "/AACS/DUPLICATE/Unit_Key_RO.inf"))
|
||||||
.map_err(|_| Error::AacsError {
|
.map_err(|_| Error::AacsError {
|
||||||
detail: "failed to read Unit_Key_RO.inf from disc".into(),
|
detail: "Unit_Key_RO.inf not found on disc".into(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Step 3: Read Content Certificate (optional — for AACS version detection)
|
|
||||||
let cc_data = udf_fs.read_file(session, "/AACS/Content000.cer")
|
let cc_data = udf_fs.read_file(session, "/AACS/Content000.cer")
|
||||||
.or_else(|_| udf_fs.read_file(session, "/AACS/Content001.cer"))
|
.or_else(|_| udf_fs.read_file(session, "/AACS/Content001.cer"))
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
// Step 4: Resolve keys
|
let mkb_data = udf_fs.read_file(session, "/AACS/MKB_RW.inf")
|
||||||
// If we have VID from handshake, use full 4-path chain.
|
.or_else(|_| udf_fs.read_file(session, "/AACS/MKB_RO.inf"))
|
||||||
// If no VID (handshake failed), use disc-hash-only KEYDB lookup.
|
.ok();
|
||||||
let mkb_data = aacs::read_mkb_from_drive(session).ok();
|
|
||||||
let mkb_ver = mkb_data.as_deref().and_then(aacs::mkb_version);
|
let mkb_ver = mkb_data.as_deref().and_then(aacs::mkb_version);
|
||||||
|
|
||||||
// Use a zero VID placeholder if handshake failed — resolve_keys
|
// Resolve: disc hash → KEYDB lookup → VUK → unit keys
|
||||||
// will still work via disc hash (path 1)
|
let vid_zero = [0u8; 16];
|
||||||
let vid_for_resolve = vid.unwrap_or([0u8; 16]);
|
|
||||||
|
|
||||||
let resolved = aacs::resolve_keys(
|
let resolved = aacs::resolve_keys(
|
||||||
&uk_ro_data,
|
&uk_ro_data,
|
||||||
cc_data.as_deref(),
|
cc_data.as_deref(),
|
||||||
&vid_for_resolve,
|
&vid_zero,
|
||||||
&keydb,
|
&keydb,
|
||||||
mkb_data.as_deref(),
|
mkb_data.as_deref(),
|
||||||
).ok_or_else(|| Error::AacsError {
|
).ok_or_else(|| Error::AacsError {
|
||||||
detail: "failed to resolve AACS keys — disc not in KEYDB".into(),
|
detail: "disc not in KEYDB".into(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let key_source = match resolved.key_source {
|
|
||||||
1 => KeySource::KeyDb,
|
|
||||||
2 => KeySource::KeyDbDerived,
|
|
||||||
3 => KeySource::ProcessingKey,
|
|
||||||
4 => KeySource::DeviceKey,
|
|
||||||
_ => KeySource::KeyDb,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(AacsState {
|
Ok(AacsState {
|
||||||
version: if resolved.aacs2 { 2 } else { 1 },
|
version: if resolved.aacs2 { 2 } else { 1 },
|
||||||
bus_encryption: resolved.bus_encryption,
|
bus_encryption: resolved.bus_encryption,
|
||||||
mkb_version: mkb_ver,
|
mkb_version: mkb_ver,
|
||||||
disc_hash: aacs::disc_hash_hex(&resolved.disc_hash),
|
disc_hash: aacs::disc_hash_hex(&resolved.disc_hash),
|
||||||
key_source,
|
key_source: match resolved.key_source {
|
||||||
|
1 => KeySource::KeyDb,
|
||||||
|
2 => KeySource::KeyDbDerived,
|
||||||
|
3 => KeySource::ProcessingKey,
|
||||||
|
4 => KeySource::DeviceKey,
|
||||||
|
_ => KeySource::KeyDb,
|
||||||
|
},
|
||||||
vuk: resolved.vuk,
|
vuk: resolved.vuk,
|
||||||
unit_keys: resolved.unit_keys,
|
unit_keys: resolved.unit_keys,
|
||||||
read_data_key,
|
read_data_key: None,
|
||||||
volume_id: vid.unwrap_or([0u8; 16]),
|
volume_id: [0u8; 16],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,16 +646,18 @@ 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;
|
||||||
|
|
||||||
// Get the m2ts file's absolute starting LBA on disc
|
// Get m2ts file start LBA and compute extent from packet count.
|
||||||
|
// BD-ROM m2ts files are contiguous on disc (mastering requirement).
|
||||||
let m2ts_path = format!("/BDMV/STREAM/{}.m2ts", play_item.clip_id);
|
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 file_lba = udf_fs.file_start_lba(session, &m2ts_path).unwrap_or(0);
|
||||||
|
let total_bytes = pkt_count as u64 * 192;
|
||||||
let mut clip_extents = clip_info.get_extents(play_item.in_time, play_item.out_time);
|
let total_sectors = ((total_bytes + 2047) / 2048) as u32;
|
||||||
// Extents from CLPI are relative to m2ts file start — add file LBA
|
if total_sectors > 0 && file_lba > 0 {
|
||||||
for ext in &mut clip_extents {
|
extents.push(Extent {
|
||||||
ext.start_lba += file_lba;
|
start_lba: file_lba,
|
||||||
|
sector_count: total_sectors,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
extents.extend(clip_extents);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,6 +741,13 @@ impl Disc {
|
|||||||
// ─── Decrypted reader ──────────────────────────────────────────────────────
|
// ─── Decrypted reader ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// A reader that reads m2ts content, decrypting transparently if needed.
|
/// A reader that reads m2ts content, decrypting transparently if needed.
|
||||||
|
///
|
||||||
|
/// Adaptive read strategy:
|
||||||
|
/// - Starts at max batch size (510 sectors ≈ 1MB) and full disc speed
|
||||||
|
/// - On read error: halves batch size, brief pause for drive recovery
|
||||||
|
/// - On repeated errors: reduces disc spin speed (scratched region)
|
||||||
|
/// - On success streak: ramps batch back up, then restores disc speed
|
||||||
|
/// - At minimum batch + still failing: retries once, then skips + zero-fills
|
||||||
pub struct ContentReader<'a> {
|
pub struct ContentReader<'a> {
|
||||||
session: &'a mut DriveSession,
|
session: &'a mut DriveSession,
|
||||||
aacs: Option<&'a AacsState>,
|
aacs: Option<&'a AacsState>,
|
||||||
@@ -792,10 +758,16 @@ pub struct ContentReader<'a> {
|
|||||||
read_buf: Vec<u8>,
|
read_buf: Vec<u8>,
|
||||||
buf_pos: usize,
|
buf_pos: usize,
|
||||||
buf_len: usize,
|
buf_len: usize,
|
||||||
/// Current batch size (adapts on errors)
|
/// Current batch size in sectors (adapts on errors)
|
||||||
batch_sectors: u16,
|
batch_sectors: u16,
|
||||||
/// Consecutive successful batch reads (for ramp-up)
|
/// Consecutive successful batch reads
|
||||||
ok_streak: u32,
|
ok_streak: u32,
|
||||||
|
/// Consecutive errors at current position
|
||||||
|
error_streak: u32,
|
||||||
|
/// Current speed tier index (0 = max, higher = slower)
|
||||||
|
speed_tier: usize,
|
||||||
|
/// Last time maintain_speed was called
|
||||||
|
last_speed_maintain: std::time::Instant,
|
||||||
/// Total read errors encountered
|
/// Total read errors encountered
|
||||||
pub errors: u32,
|
pub errors: u32,
|
||||||
}
|
}
|
||||||
@@ -821,7 +793,12 @@ 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
|
// Ensure drive is unlocked
|
||||||
|
if !session.is_unlocked() {
|
||||||
|
session.unlock()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set max read speed — nothing else. No calibration, no probes.
|
||||||
let speed_cdb = crate::scsi::build_set_cd_speed(0xFFFF);
|
let speed_cdb = crate::scsi::build_set_cd_speed(0xFFFF);
|
||||||
let mut dummy = [0u8; 0];
|
let mut dummy = [0u8; 0];
|
||||||
let _ = session.scsi_execute(&speed_cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000);
|
let _ = session.scsi_execute(&speed_cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000);
|
||||||
@@ -838,17 +815,61 @@ impl Disc {
|
|||||||
buf_len: 0,
|
buf_len: 0,
|
||||||
batch_sectors: MAX_BATCH_SECTORS,
|
batch_sectors: MAX_BATCH_SECTORS,
|
||||||
ok_streak: 0,
|
ok_streak: 0,
|
||||||
|
error_streak: 0,
|
||||||
|
speed_tier: 0,
|
||||||
|
last_speed_maintain: std::time::Instant::now(),
|
||||||
errors: 0,
|
errors: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detect the maximum transfer size in sectors for a device.
|
||||||
|
/// Reads /sys/block/<dev>/queue/max_hw_sectors_kb on Linux.
|
||||||
|
/// Returns a value aligned to 3 sectors (one aligned unit).
|
||||||
|
fn detect_max_batch_sectors(device_path: &str) -> u16 {
|
||||||
|
// Extract block device name: /dev/sr0 → sr0
|
||||||
|
let dev_name = device_path.rsplit('/').next().unwrap_or("");
|
||||||
|
if !dev_name.is_empty() {
|
||||||
|
let sysfs_path = format!("/sys/block/{}/queue/max_hw_sectors_kb", dev_name);
|
||||||
|
if let Ok(content) = std::fs::read_to_string(&sysfs_path) {
|
||||||
|
if let Ok(kb) = content.trim().parse::<u32>() {
|
||||||
|
// Convert KB to sectors (1 sector = 2 KB on disc = 2048 bytes)
|
||||||
|
let sectors = (kb / 2) as u16;
|
||||||
|
// Align down to 3 (one aligned unit) and cap at a reasonable max
|
||||||
|
let aligned = (sectors / 3) * 3;
|
||||||
|
if aligned >= MIN_BATCH_SECTORS {
|
||||||
|
return aligned.min(MAX_BATCH_SECTORS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: conservative default
|
||||||
|
MAX_BATCH_SECTORS
|
||||||
|
}
|
||||||
|
|
||||||
/// Read strategy constants
|
/// Read strategy constants
|
||||||
const MAX_BATCH_SECTORS: u16 = 96; // 32 aligned units = 192KB per command (fast)
|
const MAX_BATCH_SECTORS: u16 = 510; // 170 aligned units ≈ 1MB (kernel caps to hw limit)
|
||||||
const MIN_BATCH_SECTORS: u16 = 3; // 1 aligned unit = 6KB (slow, for error recovery)
|
const MIN_BATCH_SECTORS: u16 = 3; // 1 aligned unit = 6KB (error recovery)
|
||||||
const RAMP_UP_AFTER: u32 = 10; // successful reads before ramping back up
|
const RAMP_BATCH_AFTER: u32 = 5; // successes before doubling batch size
|
||||||
|
const RAMP_SPEED_AFTER: u32 = 50; // successes at max batch before restoring speed
|
||||||
|
const SLOW_SPEED_AFTER: u32 = 3; // consecutive errors before reducing disc speed
|
||||||
|
|
||||||
|
/// Disc speed tiers (KB/s for SET CD SPEED).
|
||||||
|
/// Blu-ray: 1x=4500, 2x=9000, 4x=18000, 8x=36000, 12x=54000
|
||||||
|
const SPEED_TIERS: &[u16] = &[
|
||||||
|
0xFFFF, // tier 0: max (drive decides, typically 8-12x)
|
||||||
|
36000, // tier 1: 8x BD (~36 MB/s)
|
||||||
|
18000, // tier 2: 4x BD (~18 MB/s)
|
||||||
|
9000, // tier 3: 2x BD (~9 MB/s)
|
||||||
|
4500, // tier 4: 1x BD (~4.5 MB/s) — last resort
|
||||||
|
];
|
||||||
|
|
||||||
impl<'a> ContentReader<'a> {
|
impl<'a> ContentReader<'a> {
|
||||||
|
/// Total bytes across all extents (for progress display).
|
||||||
|
pub fn total_bytes(&self) -> u64 {
|
||||||
|
self.extents.iter().map(|e| e.sector_count as u64 * 2048).sum()
|
||||||
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
@@ -866,36 +887,99 @@ impl<'a> ContentReader<'a> {
|
|||||||
let mut unit = self.read_buf[start..end].to_vec();
|
let mut unit = self.read_buf[start..end].to_vec();
|
||||||
|
|
||||||
// Decrypt if needed
|
// Decrypt if needed
|
||||||
|
self.decrypt_unit(&mut unit);
|
||||||
|
self.buf_pos += 1;
|
||||||
|
Ok(Some(unit))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the next batch of aligned units, decrypted in-place.
|
||||||
|
/// Returns the decrypted data as a single contiguous slice.
|
||||||
|
/// More efficient than read_unit() — one write_all() per batch instead of per unit.
|
||||||
|
/// Returns None when all extents are exhausted.
|
||||||
|
pub fn read_batch(&mut self) -> Result<Option<&[u8]>> {
|
||||||
|
if !self.fill_buffer()? {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt all units in the buffer in-place
|
||||||
|
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
||||||
if let Some(aacs) = &self.aacs {
|
if let Some(aacs) = &self.aacs {
|
||||||
if crate::aacs::is_unit_encrypted(&unit) {
|
let uk = aacs.unit_keys.get(self.unit_key_idx)
|
||||||
|
.map(|(_, k)| *k)
|
||||||
|
.unwrap_or([0u8; 16]);
|
||||||
|
let rdk = aacs.read_data_key.as_ref();
|
||||||
|
|
||||||
|
for i in 0..self.buf_len {
|
||||||
|
let start = i * unit_len;
|
||||||
|
let end = start + unit_len;
|
||||||
|
let unit = &mut self.read_buf[start..end];
|
||||||
|
if crate::aacs::is_unit_encrypted(unit) {
|
||||||
|
crate::aacs::decrypt_unit_full(unit, &uk, rdk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_bytes = self.buf_len * unit_len;
|
||||||
|
self.buf_pos = self.buf_len; // mark fully consumed
|
||||||
|
Ok(Some(&self.read_buf[..total_bytes]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt a single aligned unit in-place if needed.
|
||||||
|
fn decrypt_unit(&self, unit: &mut [u8]) {
|
||||||
|
if let Some(aacs) = &self.aacs {
|
||||||
|
if crate::aacs::is_unit_encrypted(unit) {
|
||||||
let uk = aacs.unit_keys.get(self.unit_key_idx)
|
let uk = aacs.unit_keys.get(self.unit_key_idx)
|
||||||
.map(|(_, k)| *k)
|
.map(|(_, k)| *k)
|
||||||
.unwrap_or([0u8; 16]);
|
.unwrap_or([0u8; 16]);
|
||||||
|
|
||||||
crate::aacs::decrypt_unit_full(
|
crate::aacs::decrypt_unit_full(
|
||||||
&mut unit,
|
unit,
|
||||||
&uk,
|
&uk,
|
||||||
aacs.read_data_key.as_ref(),
|
aacs.read_data_key.as_ref(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.buf_pos += 1;
|
/// Read sectors via standard READ(10) 0x00.
|
||||||
Ok(Some(unit))
|
/// calibration primers. Standard reads are faster on most drives.
|
||||||
|
fn read_sectors(&mut self, lba: u32, count: u16) -> Result<()> {
|
||||||
|
self.session.read_content(lba, count, &mut self.read_buf)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set disc spin speed via SCSI SET CD SPEED.
|
||||||
|
fn set_speed(&mut self, tier: usize) {
|
||||||
|
let tier = tier.min(SPEED_TIERS.len() - 1);
|
||||||
|
if tier != self.speed_tier {
|
||||||
|
self.speed_tier = tier;
|
||||||
|
let speed_kbs = SPEED_TIERS[tier];
|
||||||
|
let cdb = crate::scsi::build_set_cd_speed(speed_kbs);
|
||||||
|
let mut dummy = [0u8; 0];
|
||||||
|
let _ = self.session.scsi_execute(
|
||||||
|
&cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a batch of sectors into the internal buffer.
|
/// Read a batch of sectors into the internal buffer.
|
||||||
/// Adapts batch size on errors: shrinks on failure, grows on success.
|
///
|
||||||
|
/// Adaptive strategy:
|
||||||
|
/// 1. Read at current batch size
|
||||||
|
/// 2. On success: ramp batch up (double after 5 successes),
|
||||||
|
/// then restore disc speed (after 50 at max batch)
|
||||||
|
/// 3. On error: halve batch, pause. After 3 consecutive errors,
|
||||||
|
/// also reduce disc spin speed (scratched/damaged region).
|
||||||
|
/// 4. At min batch + still failing: retry once, then skip + zero-fill.
|
||||||
fn fill_buffer(&mut self) -> Result<bool> {
|
fn fill_buffer(&mut self) -> Result<bool> {
|
||||||
loop {
|
loop {
|
||||||
if self.current_extent >= self.extents.len() {
|
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);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let extent = &self.extents[self.current_extent];
|
let ext_start = self.extents[self.current_extent].start_lba;
|
||||||
let remaining = extent.sector_count - self.current_offset;
|
let ext_sectors = self.extents[self.current_extent].sector_count;
|
||||||
|
let remaining = ext_sectors - self.current_offset;
|
||||||
|
|
||||||
// Align to 3 sectors (one aligned unit)
|
// Align to 3 sectors (one aligned unit)
|
||||||
let sectors_to_read = remaining.min(self.batch_sectors as u32) as u16;
|
let sectors_to_read = remaining.min(self.batch_sectors as u32) as u16;
|
||||||
@@ -906,61 +990,72 @@ impl<'a> ContentReader<'a> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lba = extent.start_lba + self.current_offset;
|
let lba = ext_start + self.current_offset;
|
||||||
let byte_count = sectors_to_read as usize * 2048;
|
let byte_count = sectors_to_read as usize * 2048;
|
||||||
self.read_buf.resize(byte_count, 0);
|
self.read_buf.resize(byte_count, 0);
|
||||||
|
|
||||||
if self.current_offset < 100 || sectors_to_read < self.batch_sectors {
|
match self.read_sectors(lba, sectors_to_read) {
|
||||||
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(_) => {
|
Ok(_) => {
|
||||||
self.buf_len = sectors_to_read as usize / 3;
|
self.buf_len = sectors_to_read as usize / 3;
|
||||||
self.buf_pos = 0;
|
self.buf_pos = 0;
|
||||||
self.current_offset += sectors_to_read as u32;
|
self.current_offset += sectors_to_read as u32;
|
||||||
|
self.error_streak = 0;
|
||||||
|
|
||||||
if self.current_offset >= extent.sector_count {
|
if self.current_offset >= ext_sectors {
|
||||||
self.current_extent += 1;
|
self.current_extent += 1;
|
||||||
self.current_offset = 0;
|
self.current_offset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ramp up batch size after consecutive successes
|
// Ramp up: batch size first, then disc speed
|
||||||
self.ok_streak += 1;
|
self.ok_streak += 1;
|
||||||
if self.ok_streak >= RAMP_UP_AFTER && self.batch_sectors < MAX_BATCH_SECTORS {
|
if self.batch_sectors < MAX_BATCH_SECTORS {
|
||||||
|
if self.ok_streak >= RAMP_BATCH_AFTER {
|
||||||
self.batch_sectors = (self.batch_sectors * 2).min(MAX_BATCH_SECTORS);
|
self.batch_sectors = (self.batch_sectors * 2).min(MAX_BATCH_SECTORS);
|
||||||
self.ok_streak = 0;
|
self.ok_streak = 0;
|
||||||
}
|
}
|
||||||
|
} else if self.speed_tier > 0 && self.ok_streak >= RAMP_SPEED_AFTER {
|
||||||
|
// At max batch for a while — try faster disc speed
|
||||||
|
self.set_speed(self.speed_tier - 1);
|
||||||
|
self.ok_streak = 0;
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
self.errors += 1;
|
self.errors += 1;
|
||||||
|
self.error_streak += 1;
|
||||||
self.ok_streak = 0;
|
self.ok_streak = 0;
|
||||||
|
|
||||||
|
// Reduce disc speed after repeated errors (physical problem)
|
||||||
|
if self.error_streak >= SLOW_SPEED_AFTER
|
||||||
|
&& self.speed_tier < SPEED_TIERS.len() - 1
|
||||||
|
{
|
||||||
|
self.set_speed(self.speed_tier + 1);
|
||||||
|
self.error_streak = 0; // reset — give new speed a chance
|
||||||
|
}
|
||||||
|
|
||||||
if self.batch_sectors > MIN_BATCH_SECTORS {
|
if self.batch_sectors > MIN_BATCH_SECTORS {
|
||||||
// Shrink batch and retry
|
// Shrink batch and retry
|
||||||
self.batch_sectors = (self.batch_sectors / 2).max(MIN_BATCH_SECTORS);
|
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));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
} else {
|
} else {
|
||||||
// At minimum batch — retry once with a longer pause
|
// At minimum batch — retry once with longer pause
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
self.read_buf.resize(MIN_BATCH_SECTORS as usize * 2048, 0);
|
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() {
|
if self.read_sectors(lba, MIN_BATCH_SECTORS).is_ok() {
|
||||||
self.buf_len = 1;
|
self.buf_len = 1;
|
||||||
self.buf_pos = 0;
|
self.buf_pos = 0;
|
||||||
|
self.error_streak = 0;
|
||||||
self.current_offset += MIN_BATCH_SECTORS as u32;
|
self.current_offset += MIN_BATCH_SECTORS as u32;
|
||||||
if self.current_offset >= extent.sector_count {
|
if self.current_offset >= ext_sectors {
|
||||||
self.current_extent += 1;
|
self.current_extent += 1;
|
||||||
self.current_offset = 0;
|
self.current_offset = 0;
|
||||||
}
|
}
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
// Still failing — skip this unit
|
// Still failing — skip this unit (zero-fill)
|
||||||
self.current_offset += 3;
|
self.current_offset += 3;
|
||||||
if self.current_offset >= extent.sector_count {
|
if self.current_offset >= ext_sectors {
|
||||||
self.current_extent += 1;
|
self.current_extent += 1;
|
||||||
self.current_offset = 0;
|
self.current_offset = 0;
|
||||||
}
|
}
|
||||||
@@ -976,16 +1071,6 @@ impl<'a> ContentReader<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn session_read_sector(session: &mut DriveSession, lba: u32, buf: &mut [u8; 2048]) -> Result<()> {
|
|
||||||
let cdb = [
|
|
||||||
crate::scsi::SCSI_READ_10, 0x00,
|
|
||||||
(lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8,
|
|
||||||
0x00, 0x00, 0x01, 0x00,
|
|
||||||
];
|
|
||||||
session.scsi_execute(&cdb, crate::scsi::DataDirection::FromDevice, buf, 10_000)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Format helpers ────────────────────────────────────────────────────────
|
// ─── Format helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn format_resolution(video_format: u8, _video_rate: u8) -> String {
|
fn format_resolution(video_format: u8, _video_rate: u8) -> String {
|
||||||
|
|||||||
+112
-9
@@ -30,23 +30,32 @@ pub struct DriveSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DriveSession {
|
impl DriveSession {
|
||||||
/// Open a drive, identify it, match a profile, and unlock for raw reads.
|
/// Open a drive — identify, wait for disc, and unlock for raw reads.
|
||||||
///
|
///
|
||||||
/// This is the standard entry point. After `open()`, the drive is ready
|
/// This is the standard entry point. After `open()`, the drive is
|
||||||
/// for sector reads, disc scanning, and content extraction.
|
/// ready for scanning and content reads.
|
||||||
pub fn open(device: &Path) -> Result<Self> {
|
pub fn open(device: &Path) -> Result<Self> {
|
||||||
let mut session = Self::open_no_unlock(device)?;
|
let mut session = Self::open_no_unlock(device)?;
|
||||||
session.wait_ready()?;
|
session.wait_ready()?;
|
||||||
let _ = session.unlock(); // silently ignore — unencrypted discs don't need it
|
let _ = session.unlock();
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a drive WITHOUT unlocking.
|
/// Open a drive and immediately unlock for raw reads.
|
||||||
///
|
///
|
||||||
/// Used when AACS authentication must happen before raw mode.
|
/// Use this when you need raw disc access without AACS (e.g. capture,
|
||||||
/// The AACS SCSI handshake requires the drive's standard firmware
|
/// sector dumps). Skips AACS authentication — cannot be done after unlock.
|
||||||
/// state — unlocking puts the drive in vendor-specific raw mode
|
pub fn open_unlocked(device: &Path) -> Result<Self> {
|
||||||
/// which disables the AACS layer.
|
let mut session = Self::open_no_unlock(device)?;
|
||||||
|
session.wait_ready()?;
|
||||||
|
let _ = session.unlock();
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a drive — identify only, no wait, no unlock.
|
||||||
|
///
|
||||||
|
/// Low-level entry point. Caller is responsible for wait_ready()
|
||||||
|
/// and unlock() ordering.
|
||||||
pub fn open_no_unlock(device: &Path) -> Result<Self> {
|
pub fn open_no_unlock(device: &Path) -> Result<Self> {
|
||||||
let mut transport = crate::scsi::open(device)?;
|
let mut transport = crate::scsi::open(device)?;
|
||||||
let profiles = profile::load_bundled()?;
|
let profiles = profile::load_bundled()?;
|
||||||
@@ -139,6 +148,12 @@ impl DriveSession {
|
|||||||
self.platform.calibrate(self.scsi.as_mut())
|
self.platform.calibrate(self.scsi.as_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maintain read speed during bulk reading.
|
||||||
|
/// Call every ~2 seconds during ripping to prevent speed decay.
|
||||||
|
pub fn maintain_speed(&mut self, lba: u32) -> Result<()> {
|
||||||
|
self.platform.maintain_speed(self.scsi.as_mut(), lba)
|
||||||
|
}
|
||||||
|
|
||||||
/// Read raw disc sectors via platform-specific command.
|
/// Read raw disc sectors via platform-specific command.
|
||||||
pub fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
pub fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||||
self.platform.read_sectors(self.scsi.as_mut(), lba, count, buf)
|
self.platform.read_sectors(self.scsi.as_mut(), lba, count, buf)
|
||||||
@@ -205,6 +220,94 @@ impl DriveSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Discover optical drives on the system.
|
||||||
|
///
|
||||||
|
/// Scans `/dev/sg0` through `/dev/sg15` (Linux SCSI Generic devices),
|
||||||
|
/// sends INQUIRY to each, and returns paths for optical drives (device type 5).
|
||||||
|
/// Always uses sg devices — sr devices have kernel-level speed management
|
||||||
|
/// that interferes with raw disc access.
|
||||||
|
///
|
||||||
|
/// Returns a list of (device_path, DriveId) for each found drive.
|
||||||
|
pub fn find_drives() -> Vec<(String, DriveId)> {
|
||||||
|
let mut drives = Vec::new();
|
||||||
|
for i in 0..16 {
|
||||||
|
let path = format!("/dev/sg{}", i);
|
||||||
|
if !std::path::Path::new(&path).exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(mut transport) = crate::scsi::open(std::path::Path::new(&path)) {
|
||||||
|
if let Ok(id) = DriveId::from_drive(transport.as_mut()) {
|
||||||
|
// INQUIRY device type 5 = CD/DVD/BD
|
||||||
|
// We check by trying to match a profile — only optical drives have profiles
|
||||||
|
let profiles = match profile::load_bundled() {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if profile::find_by_drive_id(&profiles, &id).is_some() {
|
||||||
|
drives.push((path, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drives
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the first optical drive on the system.
|
||||||
|
/// Returns the sg device path, or None if no drive found.
|
||||||
|
pub fn find_drive() -> Option<String> {
|
||||||
|
find_drives().into_iter().next().map(|(path, _)| path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a device path to the correct sg device.
|
||||||
|
///
|
||||||
|
/// If the user passes `/dev/sr0`, maps it to the corresponding `/dev/sg*`.
|
||||||
|
/// If they pass `/dev/sg*`, validates it exists.
|
||||||
|
/// Returns `(resolved_path, warning)` where warning is set if the path was remapped.
|
||||||
|
pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
|
||||||
|
// Already an sg device — use as-is
|
||||||
|
if path.contains("/sg") {
|
||||||
|
if !std::path::Path::new(path).exists() {
|
||||||
|
return Err(Error::DeviceNotFound { path: path.to_string() });
|
||||||
|
}
|
||||||
|
return Ok((path.to_string(), None));
|
||||||
|
}
|
||||||
|
|
||||||
|
// sr device — find the matching sg device by comparing INQUIRY data
|
||||||
|
if path.contains("/sr") {
|
||||||
|
// Open the sr device to get its identity
|
||||||
|
let mut sr_transport = crate::scsi::open(std::path::Path::new(path))?;
|
||||||
|
let sr_id = DriveId::from_drive(sr_transport.as_mut())?;
|
||||||
|
drop(sr_transport);
|
||||||
|
|
||||||
|
// Find matching sg device
|
||||||
|
for (sg_path, sg_id) in find_drives() {
|
||||||
|
if sg_id.vendor_id == sr_id.vendor_id
|
||||||
|
&& sg_id.product_id == sr_id.product_id
|
||||||
|
&& sg_id.serial_number == sr_id.serial_number
|
||||||
|
{
|
||||||
|
let warning = format!(
|
||||||
|
"{} is a block device (sr) — using {} (sg) for raw access",
|
||||||
|
path, sg_path
|
||||||
|
);
|
||||||
|
return Ok((sg_path, Some(warning)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No sg match found — fall back to sr with warning
|
||||||
|
let warning = format!(
|
||||||
|
"{} is a block device (sr) — no matching sg device found, performance may be limited",
|
||||||
|
path
|
||||||
|
);
|
||||||
|
return Ok((path.to_string(), Some(warning)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown device type — use as-is
|
||||||
|
if !std::path::Path::new(path).exists() {
|
||||||
|
return Err(Error::DeviceNotFound { path: path.to_string() });
|
||||||
|
}
|
||||||
|
Ok((path.to_string(), None))
|
||||||
|
}
|
||||||
|
|
||||||
/// Create the platform-specific driver for a given chipset.
|
/// Create the platform-specific driver for a given chipset.
|
||||||
fn create_platform(profile: &DriveProfile, drive_id: &DriveId) -> Result<Box<dyn Platform>> {
|
fn create_platform(profile: &DriveProfile, drive_id: &DriveId) -> Result<Box<dyn Platform>> {
|
||||||
match profile.chipset {
|
match profile.chipset {
|
||||||
|
|||||||
+4
-3
@@ -6,10 +6,11 @@
|
|||||||
//! # Quick Start
|
//! # Quick Start
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use libfreemkv::{DriveSession, Disc, ScanOptions};
|
//! use libfreemkv::{DriveSession, Disc, ScanOptions, find_drive};
|
||||||
//! use std::path::Path;
|
//! use std::path::Path;
|
||||||
//!
|
//!
|
||||||
//! let mut session = DriveSession::open(Path::new("/dev/sr0")).unwrap();
|
//! let device = find_drive().expect("no optical drive found");
|
||||||
|
//! let mut session = DriveSession::open(Path::new(&device)).unwrap();
|
||||||
//! let disc = Disc::scan(&mut session, &ScanOptions::default()).unwrap();
|
//! let disc = Disc::scan(&mut session, &ScanOptions::default()).unwrap();
|
||||||
//!
|
//!
|
||||||
//! for title in &disc.titles {
|
//! for title in &disc.titles {
|
||||||
@@ -82,7 +83,7 @@ pub mod labels;
|
|||||||
pub mod keydb;
|
pub mod keydb;
|
||||||
|
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
pub use drive::DriveSession;
|
pub use drive::{DriveSession, find_drive, find_drives, resolve_device};
|
||||||
pub use identity::DriveId;
|
pub use identity::DriveId;
|
||||||
pub use profile::{DriveProfile, Chipset};
|
pub use profile::{DriveProfile, Chipset};
|
||||||
pub use platform::{Platform, DriveStatus};
|
pub use platform::{Platform, DriveStatus};
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ pub trait Platform {
|
|||||||
|
|
||||||
fn timing(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>;
|
fn timing(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>;
|
||||||
|
|
||||||
|
/// Continuous speed management during reading.
|
||||||
|
///
|
||||||
|
/// Called periodically (~every 2 seconds) during bulk reads.
|
||||||
|
/// Probes the current disc zone, reads drive registers, and
|
||||||
|
/// sends SET CD SPEED to maintain optimal read performance.
|
||||||
|
/// Without this, MediaTek drives drift back to 1x speed.
|
||||||
|
fn maintain_speed(&mut self, scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()>;
|
||||||
|
|
||||||
/// Check if raw disc access mode is currently enabled.
|
/// Check if raw disc access mode is currently enabled.
|
||||||
fn is_unlocked(&self) -> bool;
|
fn is_unlocked(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-50
@@ -13,13 +13,20 @@ use crate::profile::DriveProfile;
|
|||||||
use crate::scsi::{self, DataDirection, ScsiTransport};
|
use crate::scsi::{self, DataDirection, ScsiTransport};
|
||||||
use super::{Platform, DriveStatus};
|
use super::{Platform, DriveStatus};
|
||||||
|
|
||||||
|
/// BD 1x speed in KB/s — used to convert speed multipliers to SET CD SPEED values.
|
||||||
|
const BD_1X_SPEED: u16 = 4500;
|
||||||
|
|
||||||
/// MT1959 driver state.
|
/// MT1959 driver state.
|
||||||
pub struct Mt1959 {
|
pub struct Mt1959 {
|
||||||
profile: DriveProfile,
|
profile: DriveProfile,
|
||||||
mode: u8,
|
mode: u8,
|
||||||
buffer_id: u8,
|
buffer_id: u8,
|
||||||
unlocked: bool,
|
unlocked: bool,
|
||||||
speed_table: [u16; 64],
|
/// Speed table: maps disc zone (probe address >> 8) to speed in KB/s.
|
||||||
|
/// Populated by calibrate(). Entry 0 = address 0x0000, entry 255 = address 0xFF00.
|
||||||
|
speed_table: [u16; 256],
|
||||||
|
/// Total disc sectors — for mapping LBA to zone index in speed table.
|
||||||
|
disc_sectors: u32,
|
||||||
calibrated: bool,
|
calibrated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +39,8 @@ impl Mt1959 {
|
|||||||
mode,
|
mode,
|
||||||
buffer_id,
|
buffer_id,
|
||||||
unlocked: false,
|
unlocked: false,
|
||||||
speed_table: [0u16; 64],
|
speed_table: [0u16; 256],
|
||||||
|
disc_sectors: 0,
|
||||||
calibrated: false,
|
calibrated: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,24 +125,14 @@ impl Mt1959 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Look up optimal read speed for a given LBA from the calibration table.
|
/// Look up optimal read speed for a given LBA from the calibration table.
|
||||||
fn lookup_speed(&self, lba: u32) -> u16 {
|
/// Returns speed in KB/s for SET CD SPEED, or 0 if not calibrated.
|
||||||
if !self.calibrated {
|
fn lookup_speed(&self, lba: u32, disc_sectors: u32) -> u16 {
|
||||||
|
if !self.calibrated || disc_sectors == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let mut best_speed = 0u16;
|
// Map LBA to zone index (0-255). Probe address space is 0x0000-0xFF00.
|
||||||
let mut best_diff = u32::MAX;
|
let zone = ((lba as u64 * 256) / disc_sectors as u64).min(255) as usize;
|
||||||
for &entry in &self.speed_table {
|
self.speed_table[zone]
|
||||||
if entry == 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let entry_lba = entry as u32;
|
|
||||||
let diff = if lba > entry_lba { lba - entry_lba } else { entry_lba - lba };
|
|
||||||
if diff < best_diff {
|
|
||||||
best_diff = diff;
|
|
||||||
best_speed = entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
best_speed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send SET CD SPEED command.
|
/// Send SET CD SPEED command.
|
||||||
@@ -196,47 +194,57 @@ impl Platform for Mt1959 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Scans disc surface addresses via READ BUFFER sub-command 0x14 to
|
/// Probes the disc surface to build a speed profile. Each zone gets
|
||||||
/// build a 64-entry speed lookup table. Issues SET CD SPEED(max) when done.
|
/// an optimal speed in KB/s. The drive firmware returns a speed
|
||||||
|
/// multiplier (resp[0]) for each probe address.
|
||||||
|
///
|
||||||
|
/// Probe address 0x0000-0xFF00 maps linearly to the disc's LBA range.
|
||||||
|
/// resp[0] = speed multiplier (e.g. 6 = 6x BD, 12 = 12x BD).
|
||||||
fn calibrate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
fn calibrate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||||
self.ensure_unlocked(scsi)?;
|
self.ensure_unlocked(scsi)?;
|
||||||
self.validate(scsi)?;
|
self.validate(scsi)?;
|
||||||
|
|
||||||
// Initial probe: READ BUFFER sub_cmd=0x12
|
// Read disc capacity for LBA-to-zone mapping
|
||||||
let cdb = self.read_buffer_sub(0x12, 0, 4);
|
let cap_cdb = [0x25u8, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
let mut cap_buf = [0u8; 8];
|
||||||
|
if let Ok(_) = scsi.execute(&cap_cdb, DataDirection::FromDevice, &mut cap_buf, 5_000) {
|
||||||
|
self.disc_sectors = u32::from_be_bytes([cap_buf[0], cap_buf[1], cap_buf[2], cap_buf[3]]) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Calibration init — sub_cmd 0x12 with address 0x0200
|
||||||
|
// (primes the firmware for disc surface analysis)
|
||||||
|
let cdb = self.read_buffer_sub(0x12, 0x0200, 4);
|
||||||
let mut resp = [0u8; 4];
|
let mut resp = [0u8; 4];
|
||||||
let _ = scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000);
|
let _ = scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000);
|
||||||
|
|
||||||
self.validate(scsi)?;
|
// Step 2: Raw read primers — read a few sectors with 0x08 flag
|
||||||
|
// to force the drive to spin up and measure disc characteristics.
|
||||||
|
// Without these, the speed probes return stale data.
|
||||||
|
let mut primer_buf = [0u8; 2048];
|
||||||
|
let _ = scsi.execute(
|
||||||
|
&scsi::build_read10_raw(0, 1), DataDirection::FromDevice, &mut primer_buf, 30_000);
|
||||||
|
let _ = scsi.execute(
|
||||||
|
&scsi::build_read10_raw(0x200, 1), DataDirection::FromDevice, &mut primer_buf, 30_000);
|
||||||
|
let _ = scsi.execute(
|
||||||
|
&scsi::build_read10_raw(0, 1), DataDirection::FromDevice, &mut primer_buf, 30_000);
|
||||||
|
|
||||||
// Clear speed table
|
// Step 3: Speed probes — sub_cmd 0x14, addresses 0x00 through 0xFF
|
||||||
self.speed_table = [0u16; 64];
|
self.speed_table = [0u16; 256];
|
||||||
|
|
||||||
// Scan disc surface — probe addresses up to 0x10000, 256 at a time
|
for zone in 0..256u16 {
|
||||||
let mut table_idx = 0usize;
|
let cdb = self.read_buffer_sub(0x14, zone, 4);
|
||||||
let mut addr: u32 = 0;
|
|
||||||
while addr < 0x10000 && table_idx < 64 {
|
|
||||||
let cdb = self.read_buffer_sub(0x14, addr as u16, 4);
|
|
||||||
let mut resp = [0u8; 4];
|
let mut resp = [0u8; 4];
|
||||||
match scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000) {
|
match scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000) {
|
||||||
Ok(r) if r.bytes_transferred == 4 => {
|
Ok(r) if r.bytes_transferred >= 1 && resp[0] > 0 => {
|
||||||
let val = resp[0];
|
self.speed_table[zone as usize] = resp[0] as u16 * BD_1X_SPEED;
|
||||||
if val > 0 {
|
|
||||||
let speed_entry = ((resp[0] as u16) << 8) | (resp[1] as u16);
|
|
||||||
if speed_entry > 0 {
|
|
||||||
self.speed_table[table_idx] = speed_entry;
|
|
||||||
table_idx += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addr += 256;
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
addr += 256;
|
self.speed_table[zone as usize] = 0xFFFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set max speed after calibration
|
// Step 4: Set max speed
|
||||||
self.set_cd_speed(scsi, 0xFFFF)?;
|
self.set_cd_speed(scsi, 0xFFFF)?;
|
||||||
|
|
||||||
self.calibrated = true;
|
self.calibrated = true;
|
||||||
@@ -305,14 +313,7 @@ impl Platform for Mt1959 {
|
|||||||
return Err(Error::NotUnlocked);
|
return Err(Error::NotUnlocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Speed optimization from calibration
|
// No per-read speed changes — calibrate + SET CD SPEED at open_title handles it.
|
||||||
if self.calibrated {
|
|
||||||
let speed = self.lookup_speed(lba);
|
|
||||||
if speed > 0 {
|
|
||||||
let _ = self.set_cd_speed(scsi, speed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// READ(10) with raw flag 0x08
|
// READ(10) with raw flag 0x08
|
||||||
let cdb = scsi::build_read10_raw(lba, count);
|
let cdb = scsi::build_read10_raw(lba, count);
|
||||||
let result = scsi.execute(&cdb, DataDirection::FromDevice, buf, 30_000)?;
|
let result = scsi.execute(&cdb, DataDirection::FromDevice, buf, 30_000)?;
|
||||||
@@ -323,6 +324,63 @@ impl Platform for Mt1959 {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Continuous speed management — probes zone, reads registers, sets speed.
|
||||||
|
///
|
||||||
|
/// MediaTek drives decay to 1x BD speed (~5 MB/s instead of 15-20 MB/s).
|
||||||
|
///
|
||||||
|
/// Sequence (from strace analysis):
|
||||||
|
/// 1. Speed probe (sub_cmd 0x14) for current LBA zone
|
||||||
|
/// 2. Read register A (sub_cmd 0x10 at profile offset A)
|
||||||
|
/// 3. Read register B (sub_cmd 0x11 at profile offset B)
|
||||||
|
/// 4. SET CD SPEED: max → zone_speed → max
|
||||||
|
fn maintain_speed(&mut self, scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()> {
|
||||||
|
if !self.unlocked || self.disc_sectors == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Probe current zone
|
||||||
|
let zone = ((lba as u64 * 256) / self.disc_sectors as u64).min(255) as u16;
|
||||||
|
let cdb = self.read_buffer_sub(0x14, zone, 4);
|
||||||
|
let mut resp = [0u8; 4];
|
||||||
|
let _ = scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000);
|
||||||
|
let zone_speed = if resp[0] > 0 {
|
||||||
|
resp[0] as u16 * BD_1X_SPEED
|
||||||
|
} else {
|
||||||
|
0xFFFF
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Read drive registers (handlers 2 & 3)
|
||||||
|
if self.profile.register_offsets.len() >= 2 {
|
||||||
|
for i in 0..2 {
|
||||||
|
let offset = self.profile.register_offsets[i];
|
||||||
|
let sub_cmd = 0x10 + i as u8;
|
||||||
|
let cdb = [
|
||||||
|
0x3C,
|
||||||
|
self.mode,
|
||||||
|
self.buffer_id,
|
||||||
|
sub_cmd,
|
||||||
|
(offset >> 16) as u8,
|
||||||
|
(offset >> 8) as u8,
|
||||||
|
offset as u8,
|
||||||
|
0x00,
|
||||||
|
0x24, // 36 bytes
|
||||||
|
0x00,
|
||||||
|
];
|
||||||
|
let mut buf = [0u8; 36];
|
||||||
|
let _ = scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Triple SET CD SPEED: max → zone → max
|
||||||
|
let _ = self.set_cd_speed(scsi, 0xFFFF);
|
||||||
|
if zone_speed < 0xFFFF {
|
||||||
|
let _ = self.set_cd_speed(scsi, zone_speed);
|
||||||
|
}
|
||||||
|
let _ = self.set_cd_speed(scsi, 0xFFFF);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn is_unlocked(&self) -> bool {
|
fn is_unlocked(&self) -> bool {
|
||||||
self.unlocked
|
self.unlocked
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-16
@@ -194,45 +194,94 @@ impl UdfFs {
|
|||||||
|
|
||||||
/// Read an Extended File Entry (tag 266) or File Entry (tag 261)
|
/// Read an Extended File Entry (tag 266) or File Entry (tag 261)
|
||||||
/// and return its first allocation extent: (data_lba, data_length).
|
/// and return its first allocation extent: (data_lba, data_length).
|
||||||
/// The data_lba is metadata-relative.
|
/// The data_lba is partition-relative.
|
||||||
fn read_icb_extent(&self, session: &mut DriveSession, meta_lba: u32) -> Result<(u32, u32)> {
|
fn read_icb_extent(&self, session: &mut DriveSession, meta_lba: u32) -> Result<(u32, u32)> {
|
||||||
|
let extents = self.read_icb_extents(session, meta_lba)?;
|
||||||
|
extents.first().copied().ok_or_else(|| Error::DiscError {
|
||||||
|
detail: "no allocation descriptors in ICB".into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read ALL allocation extents for a file from its ICB.
|
||||||
|
/// Returns Vec of (partition_relative_lba, byte_length) pairs.
|
||||||
|
/// Handles files with many extents (e.g. 88 GB m2ts files have ~90 extents).
|
||||||
|
fn read_icb_extents(&self, session: &mut DriveSession, meta_lba: u32) -> Result<Vec<(u32, u32)>> {
|
||||||
let mut icb = [0u8; 2048];
|
let mut icb = [0u8; 2048];
|
||||||
read_sector(session, self.meta_to_abs(meta_lba), &mut icb)?;
|
read_sector(session, self.meta_to_abs(meta_lba), &mut icb)?;
|
||||||
|
|
||||||
let tag = u16::from_le_bytes([icb[0], icb[1]]);
|
let tag = u16::from_le_bytes([icb[0], icb[1]]);
|
||||||
|
|
||||||
// Get allocation descriptor offset based on ICB type
|
// Get allocation descriptor offset and total length based on ICB type
|
||||||
let ad_offset = match tag {
|
let (ad_offset, l_ad) = match tag {
|
||||||
// Extended File Entry (UDF 2.50, used by BD-ROM)
|
// Extended File Entry (UDF 2.50, used by BD-ROM)
|
||||||
// Layout: ... L_EA at [208:212], L_AD at [212:216], alloc descs at 216 + L_EA
|
|
||||||
266 => {
|
266 => {
|
||||||
let l_ea = u32::from_le_bytes([icb[208], icb[209], icb[210], icb[211]]) as usize;
|
let l_ea = u32::from_le_bytes([icb[208], icb[209], icb[210], icb[211]]) as usize;
|
||||||
216 + l_ea
|
let l_ad = u32::from_le_bytes([icb[212], icb[213], icb[214], icb[215]]) as usize;
|
||||||
|
(216 + l_ea, l_ad)
|
||||||
}
|
}
|
||||||
// Standard File Entry
|
// Standard File Entry
|
||||||
// Layout: ... L_EA at [168:172], L_AD at [172:176], alloc descs at 176 + L_EA
|
|
||||||
261 => {
|
261 => {
|
||||||
let l_ea = u32::from_le_bytes([icb[168], icb[169], icb[170], icb[171]]) as usize;
|
let l_ea = u32::from_le_bytes([icb[168], icb[169], icb[170], icb[171]]) as usize;
|
||||||
176 + l_ea
|
let l_ad = u32::from_le_bytes([icb[172], icb[173], icb[174], icb[175]]) as usize;
|
||||||
|
(176 + l_ea, l_ad)
|
||||||
}
|
}
|
||||||
_ => return Err(Error::DiscError {
|
_ => return Err(Error::DiscError {
|
||||||
detail: format!("unexpected ICB tag {} at meta_lba {}", tag, meta_lba),
|
detail: format!("unexpected ICB tag {} at meta_lba {}", tag, meta_lba),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if ad_offset + 8 > 2048 {
|
let mut extents = Vec::new();
|
||||||
return Err(Error::DiscError { detail: "ICB alloc desc out of range".into() });
|
let num_descriptors = l_ad / 8; // Short Allocation Descriptor = 8 bytes
|
||||||
|
|
||||||
|
for i in 0..num_descriptors {
|
||||||
|
let off = ad_offset + i * 8;
|
||||||
|
if off + 8 > 2048 {
|
||||||
|
break; // TODO: follow Allocation Extent Descriptors (tag 258) for overflow
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short Allocation Descriptor: extent_length(4) + extent_position(4)
|
let raw_len = u32::from_le_bytes([icb[off], icb[off + 1], icb[off + 2], icb[off + 3]]);
|
||||||
// extent_length upper 2 bits = type (0=recorded, 1=allocated not recorded, 3=next extent)
|
let extent_type = raw_len >> 30;
|
||||||
let raw_len = u32::from_le_bytes([icb[ad_offset], icb[ad_offset + 1],
|
|
||||||
icb[ad_offset + 2], icb[ad_offset + 3]]);
|
|
||||||
let data_len = raw_len & 0x3FFFFFFF;
|
let data_len = raw_len & 0x3FFFFFFF;
|
||||||
let data_lba = u32::from_le_bytes([icb[ad_offset + 4], icb[ad_offset + 5],
|
let data_lba = u32::from_le_bytes([icb[off + 4], icb[off + 5], icb[off + 6], icb[off + 7]]);
|
||||||
icb[ad_offset + 6], icb[ad_offset + 7]]);
|
|
||||||
|
|
||||||
Ok((data_lba, data_len))
|
match extent_type {
|
||||||
|
0 => extents.push((data_lba, data_len)), // recorded and allocated
|
||||||
|
1 => {} // allocated but not recorded (sparse) — skip
|
||||||
|
3 => break, // next extent of allocation descriptors — TODO
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(extents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all absolute disc sector extents for a file.
|
||||||
|
/// Returns Vec of (absolute_lba, sector_count) covering the entire file.
|
||||||
|
pub fn file_extents(&self, session: &mut DriveSession, path: &str) -> Result<Vec<(u32, 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 alloc_extents = self.read_icb_extents(session, entry.meta_lba)?;
|
||||||
|
let mut disc_extents = Vec::new();
|
||||||
|
for (lba, byte_len) in alloc_extents {
|
||||||
|
let abs_lba = self.partition_start + lba;
|
||||||
|
let sectors = ((byte_len as u64 + 2047) / 2048) as u32;
|
||||||
|
disc_extents.push((abs_lba, sectors));
|
||||||
|
}
|
||||||
|
Ok(disc_extents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user