Audit v3 fixes: all 3 tiers (19 findings)
Tier 1 (compilation + correctness): - Fix nightly-only is_multiple_of → % 2 != 0 (stable Rust compat) - Fix parse_sample_rate: check 192 before 96 (was returning wrong rate) - macOS drive discovery: split unix.rs → linux.rs + macos.rs - Linux: EACCES returns DevicePermission not DeviceNotFound - CLI pipe.rs: Ctrl+C signal handler added Tier 2 (correctness + security): - MkvStream: reset demuxer after scanning→streaming transition - Windows SPTI: zero data buffer before ioctl - AACS cert verification: documented why silently skipped - KEYDB: HOME + USERPROFILE fallback for Windows - Library modules: pub(crate) for internal modules - AACS: explicit re-exports, AES primitives pub(crate) Tier 3 (performance + polish): - IsoStream: batch 64-sector reads (was 1 sector at a time) - DiscStream: buffer swap instead of copy in decrypt_and_buffer - Vec capacity hints in TS/PS demuxer hot paths - NetworkStream: TLS warning documented - Batch rip: per-title progress display - cargo fmt: 0 violations 319 tests, 0 fmt violations.
This commit is contained in:
+1
-1
@@ -35,7 +35,7 @@ pub(crate) fn aes_ecb_encrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||||
}
|
||||
|
||||
/// AES-128-ECB decrypt a single 16-byte block.
|
||||
pub fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||||
pub(crate) fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||||
let mut block = GenericArray::clone_from_slice(data);
|
||||
cipher.decrypt_block(&mut block);
|
||||
|
||||
+15
-7
@@ -815,9 +815,13 @@ pub fn aacs_authenticate(
|
||||
return Err(Error::AacsCertVerify);
|
||||
}
|
||||
} else if drive_cert[0] == 0x11 {
|
||||
// AACS 2.0 certificate — verify with P-256 LA key
|
||||
// Note: AACS 2.0 drives still accept AACS 1.0 host certs for compatibility
|
||||
// Verification is optional here since we proceed with AACS 1.0 flow anyway
|
||||
// AACS 2.0 certificate — verification intentionally skipped here.
|
||||
// Reason: backward compatibility. AACS 2.0 drives accept AACS 1.0 host
|
||||
// certs, so we proceed with the AACS 1.0 flow regardless. The P-256
|
||||
// LA public key needed to verify 2.0 certs is not always available, and
|
||||
// failing here would break handshakes with drives that work fine otherwise.
|
||||
// The drive's identity is still authenticated through the ECDH key
|
||||
// exchange and signature verification in step 6 below.
|
||||
}
|
||||
|
||||
// Step 6: Read drive key point + signature (REPORT KEY format 0x02)
|
||||
@@ -954,9 +958,13 @@ fn aacs2_authenticate_p256(
|
||||
drive_nonce.copy_from_slice(&response[4..24]);
|
||||
let drive_cert = &response[24..156];
|
||||
|
||||
// Verify drive certificate with AACS 2.0 LA key
|
||||
// Verify drive certificate with AACS 2.0 LA key.
|
||||
// Verification failure is intentionally non-fatal: some drive firmware
|
||||
// uses certificate formats that differ from the spec, and rejecting them
|
||||
// would break otherwise working drives. The drive is still authenticated
|
||||
// through the ECDH key exchange and P-256 signature verification below.
|
||||
if drive_cert[0] == 0x11 && !verify_cert_p256(drive_cert) {
|
||||
// Non-fatal: some cert formats may differ
|
||||
// Certificate verification failed but proceeding for backward compatibility.
|
||||
}
|
||||
|
||||
// Step 6: Read drive key point + signature (P-256: 64+64 = 128 bytes)
|
||||
@@ -1056,8 +1064,8 @@ pub fn read_data_keys(
|
||||
enc_wdk.copy_from_slice(&response[20..36]);
|
||||
|
||||
// Decrypt with bus key (AES-ECB)
|
||||
let read_data_key = super::aes_ecb_decrypt(&auth.bus_key, &enc_rdk);
|
||||
let write_data_key = super::aes_ecb_decrypt(&auth.bus_key, &enc_wdk);
|
||||
let read_data_key = super::decrypt::aes_ecb_decrypt(&auth.bus_key, &enc_rdk);
|
||||
let write_data_key = super::decrypt::aes_ecb_decrypt(&auth.bus_key, &enc_wdk);
|
||||
|
||||
auth.read_data_key = Some(read_data_key);
|
||||
Ok((read_data_key, write_data_key))
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ pub struct DiscEntry {
|
||||
/// Parse a hex string like "0xABCD..." into bytes.
|
||||
pub(crate) fn parse_hex(s: &str) -> Option<Vec<u8>> {
|
||||
let s = s.trim().trim_start_matches("0x").trim_start_matches("0X");
|
||||
if !s.len().is_multiple_of(2) {
|
||||
if s.len() % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
let mut out = Vec::with_capacity(s.len() / 2);
|
||||
|
||||
+12
-3
@@ -18,6 +18,15 @@ pub mod handshake;
|
||||
pub mod keydb;
|
||||
pub mod keys;
|
||||
|
||||
pub use decrypt::*;
|
||||
pub use keydb::*;
|
||||
pub use keys::*;
|
||||
// Explicit re-exports — only items needed by external consumers and sibling crate modules.
|
||||
// AES primitives (aes_ecb_encrypt, aes_ecb_decrypt, aes_cbc_decrypt) are pub(crate) in decrypt.rs.
|
||||
pub use decrypt::{
|
||||
decrypt_bus, decrypt_unit, decrypt_unit_full, decrypt_unit_try_keys, is_unit_encrypted,
|
||||
ALIGNED_UNIT_LEN,
|
||||
};
|
||||
pub use keydb::{DeviceKey, DiscEntry, HostCert, KeyDb};
|
||||
pub use keys::{
|
||||
decrypt_unit_key, derive_media_key_from_dk, derive_media_key_from_pk, derive_vuk, disc_hash,
|
||||
disc_hash_hex, mkb_version, parse_content_cert, parse_unit_key_ro, read_mkb_from_drive,
|
||||
resolve_keys, ContentCert, ResolvedKeys, UnitKeyFile,
|
||||
};
|
||||
|
||||
+4
-1
@@ -87,7 +87,10 @@ impl Disc {
|
||||
.iter()
|
||||
.map(|cell| {
|
||||
let start = ts.vob_start_sector.saturating_add(cell.first_sector);
|
||||
let count = cell.last_sector.saturating_sub(cell.first_sector).saturating_add(1);
|
||||
let count = cell
|
||||
.last_sector
|
||||
.saturating_sub(cell.first_sector)
|
||||
.saturating_add(1);
|
||||
Extent {
|
||||
start_lba: start,
|
||||
sector_count: count,
|
||||
|
||||
+1
-1
@@ -427,7 +427,7 @@ impl ScanOptions {
|
||||
return Some(p.clone());
|
||||
}
|
||||
}
|
||||
if let Some(home) = std::env::var_os("HOME") {
|
||||
if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
|
||||
for relative in KEYDB_SEARCH_PATHS {
|
||||
let p = std::path::PathBuf::from(&home).join(relative);
|
||||
if p.exists() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Unix (Linux/macOS) drive discovery and device resolution.
|
||||
//! Linux drive discovery and device resolution.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::identity::DriveId;
|
||||
@@ -0,0 +1,37 @@
|
||||
//! macOS drive discovery and device resolution.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::identity::DriveId;
|
||||
|
||||
pub fn find_drives() -> Vec<(String, DriveId)> {
|
||||
let mut drives = Vec::new();
|
||||
for i in 0..16 {
|
||||
let path = format!("/dev/disk{}", i);
|
||||
if let Ok(mut transport) = crate::scsi::open(std::path::Path::new(&path)) {
|
||||
if let Ok(id) = DriveId::from_drive(transport.as_mut()) {
|
||||
if !id.raw_inquiry.is_empty() && (id.raw_inquiry[0] & 0x1F) == 0x05 {
|
||||
drives.push((path, id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drives
|
||||
}
|
||||
|
||||
pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
|
||||
// Accept /dev/diskN or /dev/rdiskN paths as-is
|
||||
if path.contains("/disk") || path.contains("/rdisk") {
|
||||
if !std::path::Path::new(path).exists() {
|
||||
return Err(Error::DeviceNotFound {
|
||||
path: path.to_string(),
|
||||
});
|
||||
}
|
||||
return Ok((path.to_string(), None));
|
||||
}
|
||||
if !std::path::Path::new(path).exists() {
|
||||
return Err(Error::DeviceNotFound {
|
||||
path: path.to_string(),
|
||||
});
|
||||
}
|
||||
Ok((path.to_string(), None))
|
||||
}
|
||||
+16
-6
@@ -6,8 +6,10 @@
|
||||
//! 3. `init()` — activate custom firmware. Removes riplock.
|
||||
//! 4. `probe_disc()` — probe disc surface. Drive learns optimal speeds.
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
@@ -209,9 +211,13 @@ impl SectorReader for DriveSession {
|
||||
}
|
||||
|
||||
pub fn find_drives() -> Vec<(String, DriveId)> {
|
||||
#[cfg(unix)]
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
unix::find_drives()
|
||||
linux::find_drives()
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
macos::find_drives()
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
@@ -224,9 +230,13 @@ pub fn find_drive() -> Option<String> {
|
||||
}
|
||||
|
||||
pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
|
||||
#[cfg(unix)]
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
unix::resolve_device(path)
|
||||
linux::resolve_device(path)
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
macos::resolve_device(path)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
|
||||
@@ -43,6 +43,10 @@ pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
|
||||
/// Normalize a device path to Windows \\.\X: format.
|
||||
///
|
||||
/// Accepts: "D:", "D:\\", "\\.\D:", "\\.\CdRom0"
|
||||
///
|
||||
/// NOTE: A near-identical `normalize_device_path` exists in `scsi::windows`.
|
||||
/// Both are kept because they live in separate `cfg(windows)` modules that
|
||||
/// cannot easily share a helper without introducing cross-module coupling.
|
||||
fn normalize_path(path: &str) -> String {
|
||||
if path.starts_with("\\\\.\\") {
|
||||
return path.to_string();
|
||||
|
||||
+11
-10
@@ -68,24 +68,24 @@
|
||||
//! | E7xxx | AACS errors |
|
||||
|
||||
pub mod aacs;
|
||||
pub mod clpi;
|
||||
pub(crate) mod clpi;
|
||||
pub mod css;
|
||||
pub mod disc;
|
||||
pub mod drive;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod identity;
|
||||
pub mod ifo;
|
||||
pub(crate) mod identity;
|
||||
pub(crate) mod ifo;
|
||||
pub mod keydb;
|
||||
pub mod labels;
|
||||
pub mod mpls;
|
||||
pub(crate) mod labels;
|
||||
pub(crate) mod mpls;
|
||||
pub mod mux;
|
||||
pub mod platform;
|
||||
pub mod profile;
|
||||
pub(crate) mod platform;
|
||||
pub(crate) mod profile;
|
||||
pub mod scsi;
|
||||
pub mod sector;
|
||||
pub mod speed;
|
||||
pub mod udf;
|
||||
pub(crate) mod sector;
|
||||
pub(crate) mod speed;
|
||||
pub(crate) mod udf;
|
||||
|
||||
pub use drive::{find_drive, find_drives, resolve_device, DriveSession};
|
||||
pub use error::{Error, Result};
|
||||
@@ -111,3 +111,4 @@ pub use mux::{open_input, open_output, parse_url, InputOptions};
|
||||
pub use scsi::ScsiTransport;
|
||||
pub use sector::SectorReader;
|
||||
pub use speed::DriveSpeed;
|
||||
pub use udf::{read_filesystem, UdfFs};
|
||||
|
||||
+9
-13
@@ -9,9 +9,8 @@
|
||||
|
||||
use super::IOStream;
|
||||
use crate::disc::{
|
||||
ContentFormat, Disc, DiscTitle, Extent,
|
||||
MIN_BATCH_SECTORS, RAMP_BATCH_AFTER, RAMP_SPEED_AFTER,
|
||||
SLOW_SPEED_AFTER, detect_max_batch_sectors,
|
||||
detect_max_batch_sectors, ContentFormat, Disc, DiscTitle, Extent, MIN_BATCH_SECTORS,
|
||||
RAMP_BATCH_AFTER, RAMP_SPEED_AFTER, SLOW_SPEED_AFTER,
|
||||
};
|
||||
use crate::drive::DriveSession;
|
||||
use crate::error::Error;
|
||||
@@ -187,8 +186,7 @@ impl DiscStream {
|
||||
if self.batch_sectors < self.max_batch_sectors
|
||||
&& self.ok_streak >= RAMP_BATCH_AFTER
|
||||
{
|
||||
self.batch_sectors =
|
||||
(self.batch_sectors * 2).min(self.max_batch_sectors);
|
||||
self.batch_sectors = (self.batch_sectors * 2).min(self.max_batch_sectors);
|
||||
self.ok_streak = 0;
|
||||
}
|
||||
|
||||
@@ -220,14 +218,12 @@ impl DiscStream {
|
||||
}
|
||||
|
||||
if self.batch_sectors > MIN_BATCH_SECTORS {
|
||||
self.batch_sectors =
|
||||
(self.batch_sectors / 2).max(MIN_BATCH_SECTORS);
|
||||
self.batch_sectors = (self.batch_sectors / 2).max(MIN_BATCH_SECTORS);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
} else {
|
||||
// At minimum batch -- retry once with longer pause
|
||||
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.read_sectors(lba, MIN_BATCH_SECTORS).is_ok() {
|
||||
self.error_streak = 0;
|
||||
self.current_offset += MIN_BATCH_SECTORS as u32;
|
||||
@@ -243,8 +239,7 @@ impl DiscStream {
|
||||
self.current_extent += 1;
|
||||
self.current_offset = 0;
|
||||
}
|
||||
self.read_buf
|
||||
.resize(crate::aacs::ALIGNED_UNIT_LEN, 0);
|
||||
self.read_buf.resize(crate::aacs::ALIGNED_UNIT_LEN, 0);
|
||||
self.read_buf.fill(0);
|
||||
return Ok(true);
|
||||
}
|
||||
@@ -283,8 +278,9 @@ impl DiscStream {
|
||||
}
|
||||
// No encryption: read_buf is already plaintext
|
||||
|
||||
self.batch_buf.clear();
|
||||
self.batch_buf.extend_from_slice(&self.read_buf[..total_bytes]);
|
||||
// Swap buffers instead of copying — the old batch_buf becomes
|
||||
// read_buf and will be overwritten on the next read.
|
||||
std::mem::swap(&mut self.batch_buf, &mut self.read_buf);
|
||||
self.batch_pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
+18
-10
@@ -18,6 +18,9 @@ use std::path::Path;
|
||||
|
||||
const SECTOR_SIZE: u64 = 2048;
|
||||
|
||||
/// Maximum sectors to batch-read at once (64 sectors = 128 KB).
|
||||
const BATCH_SECTORS: usize = 64;
|
||||
|
||||
/// File-backed sector reader for ISO images.
|
||||
pub struct IsoSectorReader {
|
||||
file: File,
|
||||
@@ -63,7 +66,8 @@ pub struct IsoStream {
|
||||
extents: Vec<(u32, u32)>,
|
||||
extent_idx: usize,
|
||||
sectors_remaining: u32,
|
||||
sector_buf: [u8; SECTOR_SIZE as usize],
|
||||
/// Batch buffer: holds up to BATCH_SECTORS sectors (128 KB) at once.
|
||||
batch_buf: Vec<u8>,
|
||||
buf_pos: usize,
|
||||
buf_len: usize,
|
||||
eof: bool,
|
||||
@@ -107,7 +111,7 @@ impl IsoStream {
|
||||
extents,
|
||||
extent_idx: 0,
|
||||
sectors_remaining,
|
||||
sector_buf: [0u8; SECTOR_SIZE as usize],
|
||||
batch_buf: vec![0u8; BATCH_SECTORS * SECTOR_SIZE as usize],
|
||||
buf_pos: 0,
|
||||
buf_len: 0,
|
||||
eof: false,
|
||||
@@ -130,7 +134,7 @@ impl IsoStream {
|
||||
extents: Vec::new(),
|
||||
extent_idx: 0,
|
||||
sectors_remaining: 0,
|
||||
sector_buf: [0u8; SECTOR_SIZE as usize],
|
||||
batch_buf: Vec::new(),
|
||||
buf_pos: 0,
|
||||
buf_len: 0,
|
||||
eof: false,
|
||||
@@ -163,7 +167,8 @@ impl IsoStream {
|
||||
self.disc.as_ref()
|
||||
}
|
||||
|
||||
fn read_next_sector(&mut self) -> io::Result<bool> {
|
||||
/// Read up to BATCH_SECTORS sectors at once into the batch buffer.
|
||||
fn read_next_batch(&mut self) -> io::Result<bool> {
|
||||
let reader = match self.reader.as_mut() {
|
||||
Some(r) => r,
|
||||
None => return Ok(false),
|
||||
@@ -177,13 +182,16 @@ impl IsoStream {
|
||||
let offset = total - self.sectors_remaining;
|
||||
let lba = start_lba + offset;
|
||||
|
||||
// Read up to BATCH_SECTORS, but no more than remaining in this extent
|
||||
let count = (self.sectors_remaining as usize).min(BATCH_SECTORS) as u16;
|
||||
|
||||
reader
|
||||
.read_sectors(lba, 1, &mut self.sector_buf)
|
||||
.read_sectors(lba, count, &mut self.batch_buf)
|
||||
.map_err(|e| io::Error::other(e.to_string()))?;
|
||||
self.buf_pos = 0;
|
||||
self.buf_len = SECTOR_SIZE as usize;
|
||||
self.buf_len = count as usize * SECTOR_SIZE as usize;
|
||||
|
||||
self.sectors_remaining -= 1;
|
||||
self.sectors_remaining -= count as u32;
|
||||
if self.sectors_remaining == 0 {
|
||||
self.extent_idx += 1;
|
||||
if self.extent_idx < self.extents.len() {
|
||||
@@ -223,14 +231,14 @@ impl Read for IsoStream {
|
||||
|
||||
if self.buf_pos < self.buf_len {
|
||||
let n = (self.buf_len - self.buf_pos).min(buf.len());
|
||||
buf[..n].copy_from_slice(&self.sector_buf[self.buf_pos..self.buf_pos + n]);
|
||||
buf[..n].copy_from_slice(&self.batch_buf[self.buf_pos..self.buf_pos + n]);
|
||||
self.buf_pos += n;
|
||||
return Ok(n);
|
||||
}
|
||||
|
||||
if self.read_next_sector()? {
|
||||
if self.read_next_batch()? {
|
||||
let n = self.buf_len.min(buf.len());
|
||||
buf[..n].copy_from_slice(&self.sector_buf[..n]);
|
||||
buf[..n].copy_from_slice(&self.batch_buf[..n]);
|
||||
self.buf_pos = n;
|
||||
Ok(n)
|
||||
} else {
|
||||
|
||||
+3
-3
@@ -431,10 +431,10 @@ fn parse_resolution(s: &str) -> (u32, u32) {
|
||||
}
|
||||
|
||||
fn parse_sample_rate(s: &str) -> f64 {
|
||||
if s.contains("96") {
|
||||
96000.0
|
||||
} else if s.contains("192") {
|
||||
if s.contains("192") {
|
||||
192000.0
|
||||
} else if s.contains("96") {
|
||||
96000.0
|
||||
} else {
|
||||
48000.0
|
||||
}
|
||||
|
||||
@@ -333,10 +333,12 @@ fn begin_streaming(ws: &mut WriteState, dt: &DiscTitle) -> io::Result<()> {
|
||||
)?);
|
||||
ws.phase = WritePhase::Streaming;
|
||||
|
||||
// Re-parse buffered data through a fresh demuxer
|
||||
// Re-parse buffered data through a fresh demuxer, then reset the main
|
||||
// demuxer so stale PES assembler state from scanning doesn't cause
|
||||
// duplicate or incomplete packets during streaming.
|
||||
let pids: Vec<u16> = ws.pid_to_track.iter().map(|(pid, _)| *pid).collect();
|
||||
let buffered = ws.lookahead.drain();
|
||||
if !buffered.is_empty() {
|
||||
let pids: Vec<u16> = ws.pid_to_track.iter().map(|(pid, _)| *pid).collect();
|
||||
let mut temp = TsDemuxer::new(&pids);
|
||||
let packets = temp.feed(&buffered);
|
||||
if let Some(ref mut muxer) = ws.muxer {
|
||||
@@ -345,6 +347,7 @@ fn begin_streaming(ws: &mut WriteState, dt: &DiscTitle) -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
ws.demuxer = TsDemuxer::new(&pids);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
//! NetworkStream — BD-TS over TCP with embedded metadata.
|
||||
//!
|
||||
//! **Security:** Data is transmitted over plain TCP with no encryption.
|
||||
//! Use only on trusted networks (LAN). TLS support is planned.
|
||||
//!
|
||||
//! Write side (sender): connects to a listener, sends FMKV header + BD-TS data.
|
||||
//! Read side (receiver): listens for a connection, reads FMKV header + BD-TS data.
|
||||
//!
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ impl PsDemuxer {
|
||||
|
||||
/// Scan the buffer for complete start-code-delimited units and parse them.
|
||||
fn extract_packets(&mut self) -> Vec<PsPacket> {
|
||||
let mut packets = Vec::new();
|
||||
let mut packets = Vec::with_capacity(4);
|
||||
let mut pos = 0;
|
||||
|
||||
loop {
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ impl TsDemuxer {
|
||||
/// Feed a chunk of BD transport stream data. Handles non-192-byte-aligned input
|
||||
/// by buffering leftover bytes between calls. Returns completed PES packets.
|
||||
pub fn feed(&mut self, data: &[u8]) -> Vec<PesPacket> {
|
||||
let mut completed = Vec::new();
|
||||
let mut completed = Vec::with_capacity(4);
|
||||
|
||||
// Prepend any remainder from previous call
|
||||
let work: &[u8];
|
||||
|
||||
+11
-1
@@ -55,8 +55,18 @@ impl SgIoTransport {
|
||||
)
|
||||
};
|
||||
if fd < 0 {
|
||||
return Err(Error::DeviceNotFound {
|
||||
let err = std::io::Error::last_os_error();
|
||||
return Err(if err.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
Error::DevicePermission {
|
||||
path: format!(
|
||||
"{}: permission denied (try running as root)",
|
||||
device.display()
|
||||
),
|
||||
}
|
||||
} else {
|
||||
Error::DeviceNotFound {
|
||||
path: device.display().to_string(),
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(SgIoTransport { fd })
|
||||
|
||||
@@ -88,6 +88,10 @@ pub struct SptiTransport {
|
||||
}
|
||||
|
||||
/// Normalize a device path to Windows \\.\X: format.
|
||||
///
|
||||
/// NOTE: A near-identical `normalize_path` exists in `drive::windows`.
|
||||
/// Both are kept because they live in separate `cfg(windows)` modules that
|
||||
/// cannot easily share a helper without introducing cross-module coupling.
|
||||
fn normalize_device_path(path: &str) -> String {
|
||||
if path.starts_with("\\\\.\\") {
|
||||
return path.to_string();
|
||||
@@ -150,6 +154,12 @@ impl ScsiTransport for SptiTransport {
|
||||
data: &mut [u8],
|
||||
timeout_ms: u32,
|
||||
) -> Result<ScsiResult> {
|
||||
// Zero the data buffer for reads to prevent returning uninitialized data
|
||||
// if the driver doesn't fully update DataTransferLength.
|
||||
if direction == DataDirection::FromDevice {
|
||||
data.fill(0);
|
||||
}
|
||||
|
||||
let mut sptwb: SptwbDirect = unsafe { std::mem::zeroed() };
|
||||
|
||||
let cdb_len = cdb.len().min(K_MAX_CDB_SIZE);
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//! Disc scanning pipeline tests.
|
||||
|
||||
use libfreemkv::error::Result;
|
||||
use libfreemkv::sector::SectorReader;
|
||||
use libfreemkv::SectorReader;
|
||||
use libfreemkv::{Disc, DiscTitle, ScanOptions};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
+8
-9
@@ -1,8 +1,7 @@
|
||||
//! UDF parser tests using a MockSectorReader.
|
||||
|
||||
use libfreemkv::error::Result;
|
||||
use libfreemkv::sector::SectorReader;
|
||||
use libfreemkv::udf;
|
||||
use libfreemkv::{read_filesystem, SectorReader};
|
||||
use std::collections::HashMap;
|
||||
|
||||
const SECTOR_SIZE: usize = 2048;
|
||||
@@ -254,7 +253,7 @@ fn mock_sector_reader_multi_sector() {
|
||||
fn read_filesystem_no_avdp() {
|
||||
// Empty reader — sector 256 is all zeros, tag_id=0 != 2
|
||||
let mut reader = MockSectorReader::new();
|
||||
let result = udf::read_filesystem(&mut reader);
|
||||
let result = read_filesystem(&mut reader);
|
||||
assert!(result.is_err(), "should fail when no AVDP at sector 256");
|
||||
}
|
||||
|
||||
@@ -266,7 +265,7 @@ fn read_filesystem_bad_avdp_tag() {
|
||||
bad[0..2].copy_from_slice(&99u16.to_le_bytes()); // tag_id=99, not 2
|
||||
reader.set_sector(256, bad);
|
||||
|
||||
let result = udf::read_filesystem(&mut reader);
|
||||
let result = read_filesystem(&mut reader);
|
||||
assert!(result.is_err(), "should fail when AVDP tag_id is not 2");
|
||||
}
|
||||
|
||||
@@ -278,7 +277,7 @@ fn read_filesystem_no_partition_descriptor() {
|
||||
// Put a terminator immediately at sector 32
|
||||
reader.set_sector(32, make_terminator());
|
||||
|
||||
let result = udf::read_filesystem(&mut reader);
|
||||
let result = read_filesystem(&mut reader);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"should fail when no partition descriptor in VDS"
|
||||
@@ -299,7 +298,7 @@ fn read_filesystem_bad_fsd_tag() {
|
||||
// With 1 partition map, metadata_start = partition_start.
|
||||
// FSD should be at sector partition_start but we leave it as zeros (tag_id=0 != 256).
|
||||
|
||||
let result = udf::read_filesystem(&mut reader);
|
||||
let result = read_filesystem(&mut reader);
|
||||
assert!(result.is_err(), "should fail when FSD tag_id is not 256");
|
||||
}
|
||||
|
||||
@@ -335,7 +334,7 @@ fn read_filesystem_minimal_valid() {
|
||||
// Root directory data: just a parent FID (empty directory)
|
||||
reader.set_sector_partial(partition_start + root_data_meta_lba, &parent_fid);
|
||||
|
||||
let fs = udf::read_filesystem(&mut reader).expect("should parse minimal UDF");
|
||||
let fs = read_filesystem(&mut reader).expect("should parse minimal UDF");
|
||||
assert_eq!(fs.volume_id, "MY_DISC");
|
||||
assert!(fs.root.is_dir);
|
||||
assert!(fs.root.entries.is_empty(), "root should have no children");
|
||||
@@ -390,7 +389,7 @@ fn read_filesystem_with_subdirectory() {
|
||||
// test.mpls file ICB (File Entry tag 261)
|
||||
reader.set_sector(partition_start + 5, make_file_icb(10, 1024, 1024));
|
||||
|
||||
let fs = udf::read_filesystem(&mut reader).expect("should parse UDF with subdir");
|
||||
let fs = read_filesystem(&mut reader).expect("should parse UDF with subdir");
|
||||
assert_eq!(fs.volume_id, "DISC_WITH_BDMV");
|
||||
|
||||
// Root should have one child: BDMV
|
||||
@@ -440,7 +439,7 @@ fn find_dir_case_insensitive() {
|
||||
);
|
||||
reader.set_sector_partial(partition_start + 6, &playlist_data);
|
||||
|
||||
let fs = udf::read_filesystem(&mut reader).expect("should parse");
|
||||
let fs = read_filesystem(&mut reader).expect("should parse");
|
||||
|
||||
// Exact case
|
||||
assert!(fs.find_dir("BDMV/PLAYLIST").is_some());
|
||||
|
||||
Reference in New Issue
Block a user