Clean up for public release: docs, zero warnings, no hardcoded paths
Documentation: - docs/aacs.md — AACS encryption (1.0 + 2.0), key resolution, decrypt - docs/udf.md — UDF 2.50 filesystem with metadata partitions - docs/mpls.md — MPLS playlist format, STN stream table - docs/clpi.md — CLPI clip info, EP map, sector extents - docs/architecture.md — library module map, design principles - docs/drive-access.md — drive sessions, SCSI transport, unlock Code cleanup: - Zero compiler warnings - Removed all debug eprintln from library code - No hardcoded private paths — KEYDB tests use KEYDB_PATH env var - KEYDB search locations as named constants - drive.rs: extracted create_platform(), deduplicated open methods - lib.rs: updated doc examples to show Disc::scan() API - Fixed UDF file reads (partition_start, not metadata_start) - Exported KeySource from disc module
This commit is contained in:
+17
-16
@@ -548,7 +548,7 @@ fn validate_processing_key(pk: &[u8; 16], cvalue: &[u8], _uv: &[u8], mk_dv: &[u8
|
||||
}
|
||||
|
||||
// Verify: AES-ECB(mk, mk_dv) should produce a specific pattern
|
||||
let verify = aes_ecb_encrypt(&mk, mk_dv);
|
||||
let _verify = aes_ecb_encrypt(&mk, mk_dv);
|
||||
// mk_dv verification: the first 12 bytes of AES(mk, mk_dv) should be all 0xDEADBEEF...
|
||||
// Actually per AACS spec: verify record value is AES(mk, all_zeros)
|
||||
// No — the mk_dv IS the verification value. We compute AES-ECB(mk, verify_data)
|
||||
@@ -1058,6 +1058,12 @@ pub fn decrypt_unit_full(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Get KEYDB path from KEYDB_PATH environment variable. Returns None if not set or not found.
|
||||
fn keydb_path() -> Option<std::path::PathBuf> {
|
||||
let path = std::path::PathBuf::from(std::env::var("KEYDB_PATH").ok()?);
|
||||
if path.exists() { Some(path) } else { None }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_disc_entry() {
|
||||
let line = r#"***REMOVED*** = DUNE_PART_TWO (Dune: Part Two) | D | 2024-04-02 | M | ***REMOVED*** | I | ***REMOVED*** | V | ***REMOVED*** | U | 1-***REMOVED*** ; MKBv77"#;
|
||||
@@ -1090,10 +1096,9 @@ mod tests {
|
||||
// Civil War UHD: known MK, VID, VUK from KEYDB
|
||||
// MK = 15665F98..., VID (disc_id) = from entry, VUK = F96D7908...
|
||||
// VUK = AES-DEC(MK, VID) XOR VID
|
||||
let path = std::path::Path::new("");
|
||||
if !path.exists() { return; }
|
||||
let path = match keydb_path() { Some(p) => p, None => return };
|
||||
|
||||
let db = KeyDb::load(path).unwrap();
|
||||
let db = KeyDb::load(&path).unwrap();
|
||||
|
||||
// Find a disc with both MK, disc_id, and VUK so we can verify derivation
|
||||
let entry = db.disc_entries.values()
|
||||
@@ -1221,10 +1226,9 @@ mod tests {
|
||||
fn test_decrypt_unit_key_from_vuk() {
|
||||
// Test the full chain: VUK → decrypt encrypted unit key → unit key
|
||||
// Use a known disc from KEYDB that has both VUK and unit keys
|
||||
let path = std::path::Path::new("");
|
||||
if !path.exists() { return; }
|
||||
let path = match keydb_path() { Some(p) => p, None => return };
|
||||
|
||||
let db = KeyDb::load(path).unwrap();
|
||||
let db = KeyDb::load(&path).unwrap();
|
||||
|
||||
// Find a disc with VUK and unit keys
|
||||
let entry = db.disc_entries.values()
|
||||
@@ -1261,9 +1265,8 @@ mod tests {
|
||||
assert_eq!(original.len(), ALIGNED_UNIT_LEN);
|
||||
assert!(is_unit_encrypted(&original), "Unit should be encrypted");
|
||||
|
||||
let keydb_path = std::path::Path::new("");
|
||||
if !keydb_path.exists() { return; }
|
||||
let db = KeyDb::load(keydb_path).unwrap();
|
||||
let kp = match keydb_path() { Some(p) => p, None => return };
|
||||
let db = KeyDb::load(&kp).unwrap();
|
||||
|
||||
// Civil War UHD entries
|
||||
let civil_war_entries: Vec<&DiscEntry> = db.disc_entries.values()
|
||||
@@ -1292,10 +1295,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_full_keydb() {
|
||||
let path = std::path::Path::new("");
|
||||
if !path.exists() { return; } // skip if not available
|
||||
let path = match keydb_path() { Some(p) => p, None => return }; // skip if not available
|
||||
|
||||
let db = KeyDb::load(path).unwrap();
|
||||
let db = KeyDb::load(&path).unwrap();
|
||||
|
||||
assert_eq!(db.device_keys.len(), 4);
|
||||
assert_eq!(db.processing_keys.len(), 3);
|
||||
@@ -1392,9 +1394,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_resolve_keys_vuk_path() {
|
||||
// Test the full resolve chain using VUK path
|
||||
let path = std::path::Path::new("");
|
||||
if !path.exists() { return; }
|
||||
let db = KeyDb::load(path).unwrap();
|
||||
let path = match keydb_path() { Some(p) => p, None => return };
|
||||
let db = KeyDb::load(&path).unwrap();
|
||||
|
||||
// Find V for Vendetta BD — has VUK and unit keys
|
||||
// hash: ***REMOVED***
|
||||
|
||||
@@ -23,7 +23,6 @@ use crate::drive::DriveSession;
|
||||
use crate::scsi::DataDirection;
|
||||
use num_bigint::BigUint;
|
||||
use num_traits::{One, Zero};
|
||||
use num_integer::Integer;
|
||||
use sha1::{Sha1, Digest};
|
||||
|
||||
/// Execute a SCSI command that reads data from the device.
|
||||
@@ -50,6 +49,7 @@ const EC_A: [u8; 20] = [
|
||||
0x9D, 0xC9, 0xD8, 0x13, 0x55, 0xEC, 0xCE, 0xB5, 0x60, 0xBD,
|
||||
0xB0, 0x9E, 0xF9, 0xEA, 0xE7, 0xC4, 0x79, 0xA7, 0xD7, 0xDC,
|
||||
];
|
||||
#[cfg(test)]
|
||||
const EC_B: [u8; 20] = [
|
||||
0x40, 0x2D, 0xAD, 0x3E, 0xC1, 0xCB, 0xCD, 0x16, 0x52, 0x48,
|
||||
0xD6, 0x8E, 0x12, 0x45, 0xE0, 0xC4, 0xDA, 0xAC, 0xB1, 0xD8,
|
||||
@@ -769,10 +769,13 @@ mod tests {
|
||||
#[test]
|
||||
fn test_verify_host_cert_from_keydb() {
|
||||
// Verify the host cert from our KEYDB
|
||||
let keydb_path = std::path::Path::new("");
|
||||
let keydb_path = match std::env::var("KEYDB_PATH").ok() {
|
||||
Some(p) => std::path::PathBuf::from(p),
|
||||
None => return, // skip if KEYDB_PATH not set
|
||||
};
|
||||
if !keydb_path.exists() { return; }
|
||||
|
||||
let db = crate::aacs::KeyDb::load(keydb_path).unwrap();
|
||||
let db = crate::aacs::KeyDb::load(&keydb_path).unwrap();
|
||||
if let Some(hc) = &db.host_cert {
|
||||
let valid = verify_cert(&hc.certificate);
|
||||
eprintln!("Host cert verification: {}", if valid { "PASS" } else { "FAIL" });
|
||||
|
||||
+13
-10
@@ -302,10 +302,16 @@ impl KeySource {
|
||||
|
||||
// ─── Disc scanning ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Standard KEYDB.cfg search locations (compatible with libaacs).
|
||||
const KEYDB_SEARCH_PATHS: &[&str] = &[
|
||||
".config/aacs/KEYDB.cfg", // relative to $HOME
|
||||
];
|
||||
const KEYDB_SYSTEM_PATH: &str = "/etc/aacs/KEYDB.cfg";
|
||||
|
||||
/// Options for disc scanning.
|
||||
pub struct ScanOptions {
|
||||
/// Path to KEYDB.cfg for AACS key lookup.
|
||||
/// If None, tries ~/.config/aacs/KEYDB.cfg and /etc/aacs/KEYDB.cfg.
|
||||
/// If None, searches standard locations ($HOME/.config/aacs/ and /etc/aacs/).
|
||||
pub keydb_path: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
@@ -321,17 +327,18 @@ impl ScanOptions {
|
||||
ScanOptions { keydb_path: Some(path.into()) }
|
||||
}
|
||||
|
||||
/// Resolve KEYDB path: explicit, then standard locations.
|
||||
/// Resolve KEYDB path: explicit path first, then standard locations.
|
||||
fn resolve_keydb(&self) -> Option<std::path::PathBuf> {
|
||||
if let Some(p) = &self.keydb_path {
|
||||
if p.exists() { return Some(p.clone()); }
|
||||
}
|
||||
// Standard locations
|
||||
if let Some(home) = std::env::var_os("HOME") {
|
||||
let p = std::path::PathBuf::from(home).join(".config/aacs/KEYDB.cfg");
|
||||
if p.exists() { return Some(p); }
|
||||
for relative in KEYDB_SEARCH_PATHS {
|
||||
let p = std::path::PathBuf::from(&home).join(relative);
|
||||
if p.exists() { return Some(p); }
|
||||
}
|
||||
}
|
||||
let p = std::path::PathBuf::from("/etc/aacs/KEYDB.cfg");
|
||||
let p = std::path::PathBuf::from(KEYDB_SYSTEM_PATH);
|
||||
if p.exists() { return Some(p); }
|
||||
None
|
||||
}
|
||||
@@ -426,10 +433,6 @@ impl Disc {
|
||||
detail: format!("failed to load KEYDB: {}", e),
|
||||
})?;
|
||||
|
||||
let host_cert = keydb.host_cert.as_ref().ok_or_else(|| Error::AacsError {
|
||||
detail: "no host certificate in KEYDB".into(),
|
||||
})?;
|
||||
|
||||
// Step 1: Try SCSI handshake for Volume ID + read_data_key
|
||||
// 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),
|
||||
|
||||
+66
-103
@@ -1,8 +1,13 @@
|
||||
//! High-level drive session — the main API for consumers.
|
||||
//! Drive session — open, identify, unlock, and read from optical drives.
|
||||
//!
|
||||
//! Opens a drive, identifies it via standard SCSI commands,
|
||||
//! matches it against the profile database, and provides
|
||||
//! raw disc access methods.
|
||||
//! `DriveSession` is the entry point for all drive interaction. It handles
|
||||
//! device identification, profile matching, platform-specific unlock, and
|
||||
//! provides both raw sector reads and standard SCSI command execution.
|
||||
//!
|
||||
//! Two open modes:
|
||||
//! - `open()` — identify + unlock. Ready for reading immediately.
|
||||
//! - `open_no_unlock()` — identify only. Used for AACS authentication
|
||||
//! which must happen before the drive enters raw mode.
|
||||
|
||||
use std::path::Path;
|
||||
use crate::error::{Error, Result};
|
||||
@@ -12,9 +17,10 @@ use crate::profile::{self, DriveProfile, Chipset};
|
||||
use crate::platform::{Platform, DriveStatus};
|
||||
use crate::platform::mt1959::Mt1959;
|
||||
|
||||
/// A complete drive session.
|
||||
/// A drive session with identification, platform, and SCSI transport.
|
||||
///
|
||||
/// Handles: identify → match profile → create platform → execute commands.
|
||||
/// Created via `DriveSession::open()` or `DriveSession::open_no_unlock()`.
|
||||
/// All disc reading goes through this struct.
|
||||
pub struct DriveSession {
|
||||
scsi: Box<dyn ScsiTransport>,
|
||||
platform: Box<dyn Platform>,
|
||||
@@ -24,61 +30,22 @@ pub struct DriveSession {
|
||||
}
|
||||
|
||||
impl DriveSession {
|
||||
/// Open a drive, identify it, and find the matching profile.
|
||||
/// Uses the bundled profile database — no external files needed.
|
||||
/// Open a drive, identify it, match a profile, and unlock for raw reads.
|
||||
///
|
||||
/// This is the standard entry point. After `open()`, the drive is ready
|
||||
/// for sector reads, disc scanning, and content extraction.
|
||||
pub fn open(device: &Path) -> Result<Self> {
|
||||
eprintln!(" [dbg] opening device...");
|
||||
let mut transport = crate::scsi::open(device)?;
|
||||
eprintln!(" [dbg] loading profiles...");
|
||||
let profiles = profile::load_bundled()?;
|
||||
|
||||
// Identify drive via standard SCSI commands
|
||||
// SPC-4 §6.4 (INQUIRY) + MMC-6 §5.3.10 (Feature 010Ch)
|
||||
eprintln!(" [dbg] identifying drive...");
|
||||
let drive_id = DriveId::from_drive(transport.as_mut())?;
|
||||
eprintln!(" [dbg] drive: {} {}", drive_id.vendor_id.trim(), drive_id.product_id.trim());
|
||||
|
||||
// Match drive to a profile by INQUIRY fields
|
||||
let profile = profile::find_by_drive_id(&profiles, &drive_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| Error::UnsupportedDrive {
|
||||
vendor_id: drive_id.vendor_id.trim().to_string(),
|
||||
product_id: drive_id.product_id.trim().to_string(),
|
||||
product_revision: drive_id.product_revision.trim().to_string(),
|
||||
})?;
|
||||
|
||||
let platform: Box<dyn Platform> = match profile.chipset {
|
||||
Chipset::MediaTek => {
|
||||
Box::new(Mt1959::new(profile.clone()))
|
||||
}
|
||||
Chipset::Renesas => {
|
||||
return Err(Error::UnsupportedDrive {
|
||||
vendor_id: drive_id.vendor_id.trim().to_string(),
|
||||
product_id: drive_id.product_id.trim().to_string(),
|
||||
product_revision: "Renesas not yet implemented".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let mut session = DriveSession {
|
||||
scsi: transport,
|
||||
platform,
|
||||
profile,
|
||||
drive_id,
|
||||
device_path: device.to_string_lossy().to_string(),
|
||||
};
|
||||
|
||||
// Always unlock on open — makes all reads work immediately.
|
||||
// Silently ignore failures (unencrypted discs don't need it).
|
||||
eprintln!(" [dbg] unlocking...");
|
||||
let _ = session.unlock();
|
||||
eprintln!(" [dbg] unlocked, session ready");
|
||||
|
||||
let mut session = Self::open_no_unlock(device)?;
|
||||
let _ = session.unlock(); // silently ignore — unencrypted discs don't need it
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Open a drive WITHOUT unlocking (raw mode).
|
||||
/// Used for AACS authentication which must happen before unlock.
|
||||
/// Open a drive WITHOUT unlocking.
|
||||
///
|
||||
/// Used when AACS authentication must happen before raw mode.
|
||||
/// The AACS SCSI handshake requires the drive's standard firmware
|
||||
/// state — unlocking puts the drive in vendor-specific raw mode
|
||||
/// which disables the AACS layer.
|
||||
pub fn open_no_unlock(device: &Path) -> Result<Self> {
|
||||
let mut transport = crate::scsi::open(device)?;
|
||||
let profiles = profile::load_bundled()?;
|
||||
@@ -92,16 +59,22 @@ impl DriveSession {
|
||||
product_revision: drive_id.product_revision.trim().to_string(),
|
||||
})?;
|
||||
|
||||
let platform: Box<dyn Platform> = match profile.chipset {
|
||||
Chipset::MediaTek => Box::new(Mt1959::new(profile.clone())),
|
||||
Chipset::Renesas => {
|
||||
return Err(Error::UnsupportedDrive {
|
||||
vendor_id: drive_id.vendor_id.trim().to_string(),
|
||||
product_id: drive_id.product_id.trim().to_string(),
|
||||
product_revision: "Renesas not yet implemented".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
let platform = create_platform(&profile, &drive_id)?;
|
||||
|
||||
Ok(DriveSession {
|
||||
scsi: transport,
|
||||
platform,
|
||||
profile,
|
||||
drive_id,
|
||||
device_path: device.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Open with an explicit profile, skipping auto-detection.
|
||||
pub fn open_with_profile(device: &Path, profile: DriveProfile) -> Result<Self> {
|
||||
let mut transport = crate::scsi::open(device)?;
|
||||
let drive_id = DriveId::from_drive(transport.as_mut())?;
|
||||
let platform = create_platform(&profile, &drive_id)?;
|
||||
|
||||
Ok(DriveSession {
|
||||
scsi: transport,
|
||||
@@ -117,39 +90,12 @@ impl DriveSession {
|
||||
&self.device_path
|
||||
}
|
||||
|
||||
/// Open with an explicit profile (skip auto-detection).
|
||||
pub fn open_with_profile(device: &Path, profile: DriveProfile) -> Result<Self> {
|
||||
let mut transport = crate::scsi::open(device)?;
|
||||
let drive_id = DriveId::from_drive(transport.as_mut())?;
|
||||
|
||||
let platform: Box<dyn Platform> = match profile.chipset {
|
||||
Chipset::MediaTek => {
|
||||
Box::new(Mt1959::new(profile.clone()))
|
||||
}
|
||||
Chipset::Renesas => {
|
||||
return Err(Error::UnsupportedDrive {
|
||||
vendor_id: drive_id.vendor_id.trim().to_string(),
|
||||
product_id: drive_id.product_id.trim().to_string(),
|
||||
product_revision: "Renesas not yet implemented".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DriveSession {
|
||||
scsi: transport,
|
||||
platform,
|
||||
profile,
|
||||
drive_id,
|
||||
device_path: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Activate raw disc access mode.
|
||||
/// Activate raw disc access mode (vendor-specific unlock).
|
||||
pub fn unlock(&mut self) -> Result<()> {
|
||||
self.platform.unlock(self.scsi.as_mut())
|
||||
}
|
||||
|
||||
/// Check if raw disc access mode is enabled.
|
||||
/// Check if raw disc access mode is active.
|
||||
pub fn is_unlocked(&self) -> bool {
|
||||
self.platform.is_unlocked()
|
||||
}
|
||||
@@ -174,21 +120,20 @@ impl DriveSession {
|
||||
self.platform.calibrate(self.scsi.as_mut())
|
||||
}
|
||||
|
||||
/// Read raw disc sectors.
|
||||
/// Read raw disc sectors via platform-specific command.
|
||||
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)
|
||||
}
|
||||
|
||||
/// Generic probe command.
|
||||
/// Platform-specific probe command.
|
||||
pub fn probe(&mut self, sub_cmd: u8, address: u32, length: u32) -> Result<Vec<u8>> {
|
||||
self.platform.probe(self.scsi.as_mut(), sub_cmd, address, length)
|
||||
}
|
||||
|
||||
/// Standard READ(10) — reads disc sectors for UDF filesystem, MPLS, CLPI, etc.
|
||||
/// Uses a 5-second timeout to avoid hanging on encrypted/unreadable sectors.
|
||||
/// Standard SCSI READ(10) for disc filesystem data (UDF, MPLS, CLPI).
|
||||
pub fn read_disc(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||
let cdb = [
|
||||
0x28, 0x00, // READ(10), no flags
|
||||
0x28, 0x00,
|
||||
(lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8,
|
||||
0x00,
|
||||
(count >> 8) as u8, count as u8,
|
||||
@@ -199,8 +144,26 @@ impl DriveSession {
|
||||
Ok(result.bytes_transferred)
|
||||
}
|
||||
|
||||
/// Send a raw SCSI CDB. Used by UDF reader and disc structure parsers.
|
||||
pub fn scsi_execute(&mut self, cdb: &[u8], direction: crate::scsi::DataDirection, buf: &mut [u8], timeout_ms: u32) -> Result<crate::scsi::ScsiResult> {
|
||||
/// Execute a raw SCSI CDB. Used by parsers and AACS handshake.
|
||||
pub fn scsi_execute(
|
||||
&mut self,
|
||||
cdb: &[u8],
|
||||
direction: crate::scsi::DataDirection,
|
||||
buf: &mut [u8],
|
||||
timeout_ms: u32,
|
||||
) -> Result<crate::scsi::ScsiResult> {
|
||||
self.scsi.as_mut().execute(cdb, direction, buf, timeout_ms)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the platform-specific driver for a given chipset.
|
||||
fn create_platform(profile: &DriveProfile, drive_id: &DriveId) -> Result<Box<dyn Platform>> {
|
||||
match profile.chipset {
|
||||
Chipset::MediaTek => Ok(Box::new(Mt1959::new(profile.clone()))),
|
||||
Chipset::Renesas => Err(Error::UnsupportedDrive {
|
||||
vendor_id: drive_id.vendor_id.trim().to_string(),
|
||||
product_id: drive_id.product_id.trim().to_string(),
|
||||
product_revision: "Renesas not yet implemented".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
+41
-42
@@ -1,72 +1,70 @@
|
||||
//! libfreemkv — Open source optical drive library for 4K UHD / Blu-ray / DVD.
|
||||
//!
|
||||
//! Drive access, disc format parsing, and raw sector reading in one library.
|
||||
//! 206 bundled drive profiles. No external files, no configuration.
|
||||
//! Handles drive access, disc structure parsing, AACS decryption, and raw
|
||||
//! sector reading. 206 bundled drive profiles. No external files needed.
|
||||
//!
|
||||
//! # Drive Access
|
||||
//! # Quick Start
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use libfreemkv::DriveSession;
|
||||
//! use libfreemkv::{DriveSession, Disc, ScanOptions};
|
||||
//! use std::path::Path;
|
||||
//!
|
||||
//! // Open drive — profiles are bundled, auto-identify
|
||||
//! let mut session = DriveSession::open(Path::new("/dev/sr0")).unwrap();
|
||||
//! let disc = Disc::scan(&mut session, &ScanOptions::default()).unwrap();
|
||||
//!
|
||||
//! // Drive identity
|
||||
//! println!("{} {}", session.drive_id.vendor_id.trim(), session.drive_id.product_id.trim());
|
||||
//! for title in &disc.titles {
|
||||
//! println!("{} — {} streams", title.duration_display(), title.streams.len());
|
||||
//! }
|
||||
//!
|
||||
//! // Unlock and read raw sectors
|
||||
//! session.unlock().unwrap();
|
||||
//! session.calibrate().unwrap();
|
||||
//! let mut buf = vec![0u8; 2048];
|
||||
//! session.read_sectors(0, 1, &mut buf).unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! # Disc Scanning
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use libfreemkv::{DriveSession, Disc, Title, Stream, StreamKind};
|
||||
//! # use std::path::Path;
|
||||
//! # let mut session = DriveSession::open(Path::new("/dev/sr0")).unwrap();
|
||||
//! // Scan disc structure — UDF filesystem, MPLS playlists, CLPI clip info
|
||||
//! // (API in progress — Disc::scan() coming soon)
|
||||
//!
|
||||
//! // Each title has typed streams:
|
||||
//! // stream.codec → Codec::Hevc / Codec::TrueHd / Codec::Ac3 / Codec::Pgs
|
||||
//! // stream.pid → 0x1100
|
||||
//! // stream.language → "eng"
|
||||
//! // stream.hdr → HdrFormat::Hdr10 / HdrFormat::DolbyVision
|
||||
//! // Read content (decrypted automatically if AACS keys available)
|
||||
//! let mut reader = disc.open_title(&mut session, 0).unwrap();
|
||||
//! while let Some(unit) = reader.read_unit().unwrap() {
|
||||
//! // 6144 bytes of decrypted content per unit
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! ```text
|
||||
//! DriveSession — open, identify, unlock, read sectors
|
||||
//! ├── ScsiTransport — SG_IO (Linux), IOKit (macOS planned)
|
||||
//! ├── DriveProfile — per-drive unlock parameters (206 bundled)
|
||||
//! ├── DriveId — INQUIRY + GET_CONFIG 010C identification
|
||||
//! DriveSession — open, identify, unlock, read sectors
|
||||
//! ├── ScsiTransport — SG_IO (Linux), IOKit (macOS planned)
|
||||
//! ├── DriveProfile — per-drive unlock parameters (206 bundled)
|
||||
//! ├── DriveId — INQUIRY + GET_CONFIG identification
|
||||
//! └── Platform
|
||||
//! └── Mt1959 — MediaTek unlock/read (Renesas planned)
|
||||
//! └── Mt1959 — MediaTek unlock/read (Renesas planned)
|
||||
//!
|
||||
//! Disc — scan titles, streams, sector ranges
|
||||
//! ├── UDF reader — Blu-ray UDF 2.50 with metadata partitions
|
||||
//! ├── MPLS parser — playlists → titles + clips + STN streams
|
||||
//! └── CLPI parser — clip info → EP map → sector extents
|
||||
//! Disc — scan titles, streams, AACS state
|
||||
//! ├── UDF reader — Blu-ray UDF 2.50 with metadata partitions
|
||||
//! ├── MPLS parser — playlists → titles + clips + STN streams
|
||||
//! ├── CLPI parser — clip info → EP map → sector extents
|
||||
//! ├── JAR parser — BD-J audio track labels
|
||||
//! └── AACS — encryption: key resolution + content decrypt
|
||||
//! ├── aacs — KEYDB, VUK, MKB, unit decrypt
|
||||
//! └── handshake — SCSI auth, ECDH, bus key
|
||||
//! ```
|
||||
//!
|
||||
//! # AACS Encryption
|
||||
//!
|
||||
//! Disc scanning automatically detects and handles AACS encryption.
|
||||
//! If a KEYDB.cfg is available (via `ScanOptions` or standard paths),
|
||||
//! the library resolves keys and decrypts content transparently.
|
||||
//!
|
||||
//! Supports AACS 1.0 (Blu-ray) and AACS 2.0 (UHD, with fallback).
|
||||
//!
|
||||
//! # Error Codes
|
||||
//!
|
||||
//! All errors are structured with numeric codes (E1000-E6000).
|
||||
//! No user-facing English text — applications format their own messages.
|
||||
//! All errors are structured with numeric codes. No user-facing English
|
||||
//! text — applications format their own messages.
|
||||
//!
|
||||
//! | Range | Category |
|
||||
//! |-------|----------|
|
||||
//! | E1xxx | Device errors (not found, permission) |
|
||||
//! | E2xxx | Profile errors (unsupported drive, parse) |
|
||||
//! | E3xxx | Unlock errors (failed, signature mismatch) |
|
||||
//! | E2xxx | Profile errors (unsupported drive) |
|
||||
//! | E3xxx | Unlock errors (failed, signature) |
|
||||
//! | E4xxx | SCSI errors (command failed, timeout) |
|
||||
//! | E5xxx | I/O errors |
|
||||
//! | E6xxx | Disc format errors |
|
||||
//! | E7xxx | AACS errors |
|
||||
|
||||
pub mod error;
|
||||
pub mod scsi;
|
||||
@@ -90,4 +88,5 @@ pub use profile::{DriveProfile, Chipset};
|
||||
pub use platform::{Platform, DriveStatus};
|
||||
pub use scsi::ScsiTransport;
|
||||
pub use speed::DriveSpeed;
|
||||
pub use disc::{Disc, Title, Stream, StreamKind, Codec, HdrFormat, ColorSpace, Extent, ContentReader, AacsState, ScanOptions};
|
||||
pub use disc::{Disc, Title, Stream, StreamKind, Codec, HdrFormat, ColorSpace,
|
||||
Extent, ContentReader, AacsState, KeySource, ScanOptions};
|
||||
|
||||
@@ -141,10 +141,6 @@ impl ScsiTransport for SgIoTransport {
|
||||
|
||||
if hdr.status != 0 {
|
||||
let sense_key = if hdr.sb_len_wr > 2 { sense[2] & 0x0F } else { 0 };
|
||||
let asc = if hdr.sb_len_wr > 12 { sense[12] } else { 0 };
|
||||
let ascq = if hdr.sb_len_wr > 13 { sense[13] } else { 0 };
|
||||
eprintln!(" [scsi] CDB {:02x?} failed: status=0x{:02x} sense={:02x}/{:02x}/{:02x}",
|
||||
&cdb[..cdb.len().min(12)], hdr.status, sense_key, asc, ascq);
|
||||
return Err(Error::ScsiError {
|
||||
opcode: cdb[0],
|
||||
status: hdr.status,
|
||||
|
||||
+1
-3
@@ -65,9 +65,7 @@ impl DriveSpeed {
|
||||
22_001..=31_000 => DriveSpeed::BD6x,
|
||||
31_001..=40_000 => DriveSpeed::BD8x,
|
||||
40_001..=49_000 => DriveSpeed::BD10x,
|
||||
49_001..=0xFFFE => DriveSpeed::BD12x,
|
||||
0xFFFF => DriveSpeed::Max,
|
||||
_ => DriveSpeed::Max,
|
||||
49_001..=u16::MAX => DriveSpeed::BD12x,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -221,7 +221,7 @@ pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
|
||||
|
||||
// Parse partition maps starting at offset 440
|
||||
// Map 0 = Type 1 (physical), Map 1 = Type 2 (metadata)
|
||||
let pm1_type = lvd[440]; // First map type
|
||||
let _pm1_type = lvd[440]; // First map type
|
||||
let pm1_len = lvd[441] as usize;
|
||||
|
||||
if pm1_len > 0 && 440 + pm1_len < 2048 {
|
||||
|
||||
Reference in New Issue
Block a user