v0.6.0: Clean API, chipset-keyed profiles, streamlined platform driver
- API: open() is OEM-only, wait_ready() separate, init() optional
- Profiles: chipset-keyed JSON ({ "mt1959": [...], "renesas": [] })
- Profiles: identity group, variant + signature + firmware per drive
- Platform constants: mode, buffer_id, nominal speed, verify commands
moved from profiles to code (variant-determined, not per-drive)
- Removed unused fields: register CDBs, speed tables, status data
- Platform driver: unlock + firmware upload + calibrate + speed only
- Cross-compile fix: build.rs uses CARGO_CFG_TARGET_OS for framework linking
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
#[cfg(target_os = "macos")]
|
let target = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
{
|
if target == "macos" {
|
||||||
println!("cargo:rustc-link-lib=framework=IOKit");
|
println!("cargo:rustc-link-lib=framework=IOKit");
|
||||||
println!("cargo:rustc-link-lib=framework=CoreFoundation");
|
println!("cargo:rustc-link-lib=framework=CoreFoundation");
|
||||||
}
|
}
|
||||||
|
|||||||
+2271
-5358
File diff suppressed because it is too large
Load Diff
+3
-5
@@ -802,11 +802,9 @@ 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()),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// init() already called by DriveSession::open(). No re-init needed.
|
// Let the drive manage its own read speed after init.
|
||||||
|
// SET_CD_SPEED is only used reactively by the error handler to slow
|
||||||
let speed_cdb = crate::scsi::build_set_cd_speed(0xFFFF);
|
// down on read errors, then let the drive recover.
|
||||||
let mut dummy = [0u8; 0];
|
|
||||||
let _ = session.scsi_execute(&speed_cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000);
|
|
||||||
|
|
||||||
// Detect kernel max transfer size for this device
|
// Detect kernel max transfer size for this device
|
||||||
let max_batch = detect_max_batch_sectors(session.device_path());
|
let max_batch = detect_max_batch_sectors(session.device_path());
|
||||||
|
|||||||
+26
-78
@@ -1,85 +1,59 @@
|
|||||||
//! Drive session — open, identify, unlock, and read from optical drives.
|
//! Drive session — open, identify, and read from optical drives.
|
||||||
//!
|
//!
|
||||||
//! `DriveSession` is the entry point for all drive interaction. It handles
|
//! `DriveSession` is the entry point for all drive interaction. It handles
|
||||||
//! device identification, profile matching, platform-specific unlock, and
|
//! device identification, profile matching, and provides both raw sector
|
||||||
//! provides both raw sector reads and standard SCSI command execution.
|
//! reads and standard SCSI command execution.
|
||||||
//!
|
//!
|
||||||
//! Two open modes:
|
//! Three-step open:
|
||||||
//! - `open()` — identify + unlock. Ready for reading immediately.
|
//! 1. `open()` — open device, identify drive. Always OEM.
|
||||||
//! - `open_no_unlock()` — identify only. Used for AACS authentication
|
//! 2. `wait_ready()` — wait for disc to spin up. Call before reading.
|
||||||
//! which must happen before the drive enters raw mode.
|
//! 3. `init()` — activate custom firmware. Optional, caller decides.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::scsi::ScsiTransport;
|
use crate::scsi::ScsiTransport;
|
||||||
use crate::identity::DriveId;
|
use crate::identity::DriveId;
|
||||||
use crate::profile::{self, DriveProfile, Chipset};
|
use crate::profile::{self, DriveProfile, Chipset, ProfileMatch};
|
||||||
use crate::platform::Platform;
|
use crate::platform::Platform;
|
||||||
use crate::platform::mt1959::Mt1959;
|
use crate::platform::mt1959::Mt1959;
|
||||||
|
|
||||||
/// A drive session with identification, platform, and SCSI transport.
|
/// A drive session with identification, platform, and SCSI transport.
|
||||||
///
|
///
|
||||||
/// Created via `DriveSession::open()` or `DriveSession::open_no_unlock()`.
|
/// Created via `DriveSession::open()`.
|
||||||
/// All disc reading goes through this struct.
|
/// All disc reading goes through this struct.
|
||||||
pub struct DriveSession {
|
pub struct DriveSession {
|
||||||
scsi: Box<dyn ScsiTransport>,
|
scsi: Box<dyn ScsiTransport>,
|
||||||
platform: Box<dyn Platform>,
|
platform: Box<dyn Platform>,
|
||||||
pub profile: DriveProfile,
|
pub profile: DriveProfile,
|
||||||
|
pub chipset: Chipset,
|
||||||
pub drive_id: DriveId,
|
pub drive_id: DriveId,
|
||||||
device_path: String,
|
device_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DriveSession {
|
impl DriveSession {
|
||||||
|
/// Open a drive — SCSI transport + INQUIRY identify.
|
||||||
///
|
///
|
||||||
/// This is the only entry point. After `open()`, the drive is
|
/// Pure OEM. No disc needed, no custom firmware.
|
||||||
/// ready for scanning and content reads. init() handles everything:
|
/// Call `wait_ready()` before reading, `init()` for custom firmware.
|
||||||
/// unlock, firmware upload if needed, calibration, registers.
|
|
||||||
/// Called once per session. Non-fatal if init fails (BD works without it).
|
|
||||||
pub fn open(device: &Path) -> Result<Self> {
|
pub fn open(device: &Path) -> Result<Self> {
|
||||||
let mut session = Self::open_no_unlock(device)?;
|
|
||||||
session.wait_ready()?;
|
|
||||||
let _ = session.init();
|
|
||||||
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> {
|
|
||||||
let mut transport = crate::scsi::open(device)?;
|
let mut transport = crate::scsi::open(device)?;
|
||||||
let profiles = profile::load_bundled()?;
|
let profiles = profile::load_bundled()?;
|
||||||
let drive_id = DriveId::from_drive(transport.as_mut())?;
|
let drive_id = DriveId::from_drive(transport.as_mut())?;
|
||||||
|
|
||||||
let profile = profile::find_by_drive_id(&profiles, &drive_id)
|
let m = profile::find_by_drive_id(&profiles, &drive_id)
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| Error::UnsupportedDrive {
|
.ok_or_else(|| Error::UnsupportedDrive {
|
||||||
vendor_id: drive_id.vendor_id.trim().to_string(),
|
vendor_id: drive_id.vendor_id.trim().to_string(),
|
||||||
product_id: drive_id.product_id.trim().to_string(),
|
product_id: drive_id.product_id.trim().to_string(),
|
||||||
product_revision: drive_id.product_revision.trim().to_string(),
|
product_revision: drive_id.product_revision.trim().to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let platform = create_platform(&profile, &drive_id)?;
|
let platform = create_platform(m.chipset, &m.profile)?;
|
||||||
|
|
||||||
Ok(DriveSession {
|
Ok(DriveSession {
|
||||||
scsi: transport,
|
scsi: transport,
|
||||||
platform,
|
platform,
|
||||||
profile,
|
chipset: m.chipset,
|
||||||
drive_id,
|
profile: m.profile,
|
||||||
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,
|
|
||||||
platform,
|
|
||||||
profile,
|
|
||||||
drive_id,
|
drive_id,
|
||||||
device_path: device.to_string_lossy().to_string(),
|
device_path: device.to_string_lossy().to_string(),
|
||||||
})
|
})
|
||||||
@@ -88,7 +62,7 @@ impl DriveSession {
|
|||||||
/// Wait for the drive to become ready (disc spun up).
|
/// Wait for the drive to become ready (disc spun up).
|
||||||
/// Polls TEST UNIT READY up to 30 seconds.
|
/// Polls TEST UNIT READY up to 30 seconds.
|
||||||
pub fn wait_ready(&mut self) -> Result<()> {
|
pub fn wait_ready(&mut self) -> Result<()> {
|
||||||
let tur = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; // TEST UNIT READY
|
let tur = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||||||
for _ in 0..60 {
|
for _ in 0..60 {
|
||||||
let mut buf = [0u8; 0];
|
let mut buf = [0u8; 0];
|
||||||
if self.scsi.as_mut().execute(
|
if self.scsi.as_mut().execute(
|
||||||
@@ -108,10 +82,10 @@ impl DriveSession {
|
|||||||
&self.device_path
|
&self.device_path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Activate custom firmware — unlock, upload firmware if needed, calibrate.
|
||||||
///
|
///
|
||||||
/// This is the ONLY entry point for activating raw disc access.
|
/// Optional. BD/DVD work without this (OEM, standard speed).
|
||||||
/// Handles the full x86 dispatch sequence internally:
|
/// Required for UHD (AACS 2.0 bus encryption).
|
||||||
/// unlock → [load_firmware if cold] × 6 → calibrate × 6 → registers
|
|
||||||
pub fn init(&mut self) -> Result<()> {
|
pub fn init(&mut self) -> Result<()> {
|
||||||
self.platform.init(self.scsi.as_mut())
|
self.platform.init(self.scsi.as_mut())
|
||||||
}
|
}
|
||||||
@@ -121,7 +95,7 @@ impl DriveSession {
|
|||||||
self.platform.is_ready()
|
self.platform.is_ready()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called per zone change during content reads.
|
/// Set read speed for a disc zone.
|
||||||
pub fn set_read_speed(&mut self, lba: u32) -> Result<()> {
|
pub fn set_read_speed(&mut self, lba: u32) -> Result<()> {
|
||||||
self.platform.set_read_speed(self.scsi.as_mut(), lba)
|
self.platform.set_read_speed(self.scsi.as_mut(), lba)
|
||||||
}
|
}
|
||||||
@@ -155,16 +129,11 @@ impl DriveSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Eject the disc tray.
|
/// Eject the disc tray.
|
||||||
///
|
|
||||||
/// Sends PREVENT ALLOW MEDIUM REMOVAL (allow) first to release any
|
|
||||||
/// locks, then START STOP UNIT with LoEj=1 to open the tray.
|
|
||||||
pub fn eject(&mut self) -> Result<()> {
|
pub fn eject(&mut self) -> Result<()> {
|
||||||
// PREVENT ALLOW MEDIUM REMOVAL: allow removal
|
|
||||||
let allow_cdb = [0x1Eu8, 0, 0, 0, 0x00, 0];
|
let allow_cdb = [0x1Eu8, 0, 0, 0, 0x00, 0];
|
||||||
let mut buf = [0u8; 0];
|
let mut buf = [0u8; 0];
|
||||||
let _ = self.scsi.as_mut().execute(&allow_cdb, crate::scsi::DataDirection::None, &mut buf, 5_000);
|
let _ = self.scsi.as_mut().execute(&allow_cdb, crate::scsi::DataDirection::None, &mut buf, 5_000);
|
||||||
|
|
||||||
// START STOP UNIT: LoEj=1, Start=0
|
|
||||||
let eject_cdb = [0x1Bu8, 0, 0, 0, 0x02, 0];
|
let eject_cdb = [0x1Bu8, 0, 0, 0, 0x02, 0];
|
||||||
self.scsi.as_mut().execute(&eject_cdb, crate::scsi::DataDirection::None, &mut buf, 30_000)?;
|
self.scsi.as_mut().execute(&eject_cdb, crate::scsi::DataDirection::None, &mut buf, 30_000)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -183,13 +152,6 @@ impl DriveSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Discover optical drives on the system.
|
/// 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)> {
|
pub fn find_drives() -> Vec<(String, DriveId)> {
|
||||||
let mut drives = Vec::new();
|
let mut drives = Vec::new();
|
||||||
for i in 0..16 {
|
for i in 0..16 {
|
||||||
@@ -199,8 +161,6 @@ pub fn find_drives() -> Vec<(String, DriveId)> {
|
|||||||
}
|
}
|
||||||
if let Ok(mut transport) = crate::scsi::open(std::path::Path::new(&path)) {
|
if let Ok(mut transport) = crate::scsi::open(std::path::Path::new(&path)) {
|
||||||
if let Ok(id) = DriveId::from_drive(transport.as_mut()) {
|
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() {
|
let profiles = match profile::load_bundled() {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
@@ -215,18 +175,12 @@ pub fn find_drives() -> Vec<(String, DriveId)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Find the first optical drive on the system.
|
/// 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> {
|
pub fn find_drive() -> Option<String> {
|
||||||
find_drives().into_iter().next().map(|(path, _)| path)
|
find_drives().into_iter().next().map(|(path, _)| path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a device path to the correct sg device.
|
/// 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>)> {
|
pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
|
||||||
// Already an sg device — use as-is
|
|
||||||
if path.contains("/sg") {
|
if path.contains("/sg") {
|
||||||
if !std::path::Path::new(path).exists() {
|
if !std::path::Path::new(path).exists() {
|
||||||
return Err(Error::DeviceNotFound { path: path.to_string() });
|
return Err(Error::DeviceNotFound { path: path.to_string() });
|
||||||
@@ -234,14 +188,11 @@ pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
|
|||||||
return Ok((path.to_string(), None));
|
return Ok((path.to_string(), None));
|
||||||
}
|
}
|
||||||
|
|
||||||
// sr device — find the matching sg device by comparing INQUIRY data
|
|
||||||
if path.contains("/sr") {
|
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 mut sr_transport = crate::scsi::open(std::path::Path::new(path))?;
|
||||||
let sr_id = DriveId::from_drive(sr_transport.as_mut())?;
|
let sr_id = DriveId::from_drive(sr_transport.as_mut())?;
|
||||||
drop(sr_transport);
|
drop(sr_transport);
|
||||||
|
|
||||||
// Find matching sg device
|
|
||||||
for (sg_path, sg_id) in find_drives() {
|
for (sg_path, sg_id) in find_drives() {
|
||||||
if sg_id.vendor_id == sr_id.vendor_id
|
if sg_id.vendor_id == sr_id.vendor_id
|
||||||
&& sg_id.product_id == sr_id.product_id
|
&& sg_id.product_id == sr_id.product_id
|
||||||
@@ -255,7 +206,6 @@ pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No sg match found — fall back to sr with warning
|
|
||||||
let warning = format!(
|
let warning = format!(
|
||||||
"{} is a block device (sr) — no matching sg device found, performance may be limited",
|
"{} is a block device (sr) — no matching sg device found, performance may be limited",
|
||||||
path
|
path
|
||||||
@@ -263,20 +213,18 @@ pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
|
|||||||
return Ok((path.to_string(), Some(warning)));
|
return Ok((path.to_string(), Some(warning)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown device type — use as-is
|
|
||||||
if !std::path::Path::new(path).exists() {
|
if !std::path::Path::new(path).exists() {
|
||||||
return Err(Error::DeviceNotFound { path: path.to_string() });
|
return Err(Error::DeviceNotFound { path: path.to_string() });
|
||||||
}
|
}
|
||||||
Ok((path.to_string(), None))
|
Ok((path.to_string(), None))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create the platform-specific driver for a given chipset.
|
fn create_platform(chipset: Chipset, profile: &DriveProfile) -> Result<Box<dyn Platform>> {
|
||||||
fn create_platform(profile: &DriveProfile, drive_id: &DriveId) -> Result<Box<dyn Platform>> {
|
match chipset {
|
||||||
match profile.chipset {
|
|
||||||
Chipset::MediaTek => Ok(Box::new(Mt1959::new(profile.clone()))),
|
Chipset::MediaTek => Ok(Box::new(Mt1959::new(profile.clone()))),
|
||||||
Chipset::Renesas => Err(Error::UnsupportedDrive {
|
Chipset::Renesas => Err(Error::UnsupportedDrive {
|
||||||
vendor_id: drive_id.vendor_id.trim().to_string(),
|
vendor_id: profile.identity.vendor_id.trim().to_string(),
|
||||||
product_id: drive_id.product_id.trim().to_string(),
|
product_id: String::new(),
|
||||||
product_revision: "Renesas not yet implemented".to_string(),
|
product_revision: "Renesas not yet implemented".to_string(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-18
@@ -1,11 +1,8 @@
|
|||||||
//! Platform-specific drive initialization and speed management.
|
//! Platform-specific drive initialization and speed management.
|
||||||
//!
|
//!
|
||||||
//! The Platform trait is minimal by design. Callers cannot access
|
//! The Platform trait is minimal by design. Callers use init() once,
|
||||||
//! individual handlers (unlock, firmware upload, calibrate) directly.
|
//! then set_read_speed() during reads. Internal operations cannot be
|
||||||
//! This prevents out-of-sequence operations that could damage drives.
|
//! called directly — this prevents out-of-sequence operations.
|
||||||
//!
|
|
||||||
//! Pipeline: DriveSession::open() calls init() once. After that,
|
|
||||||
//! only set_read_speed() is available during reads.
|
|
||||||
|
|
||||||
pub mod mt1959;
|
pub mod mt1959;
|
||||||
|
|
||||||
@@ -15,21 +12,11 @@ use crate::scsi::ScsiTransport;
|
|||||||
/// Platform trait — locked-down interface.
|
/// Platform trait — locked-down interface.
|
||||||
///
|
///
|
||||||
/// Only three operations exposed:
|
/// Only three operations exposed:
|
||||||
/// init() — called once by DriveSession::open()
|
/// init() — one-time initialization
|
||||||
/// set_read_speed() — called per zone during reads
|
/// set_read_speed() — per-zone speed during reads
|
||||||
/// is_ready() — state check
|
/// is_ready() — state check
|
||||||
///
|
|
||||||
/// All internal handlers (unlock, firmware, calibrate, registers)
|
|
||||||
/// are private to the implementation. Cannot be called externally.
|
|
||||||
pub(crate) trait Platform {
|
pub(crate) trait Platform {
|
||||||
/// One-time initialization. Called by DriveSession::open() only.
|
|
||||||
/// Internally: unlock → [firmware if needed] → calibrate → registers.
|
|
||||||
/// Safe to call on any drive state (warm, cold, OEM).
|
|
||||||
fn init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>;
|
fn init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>;
|
||||||
|
|
||||||
/// Set read speed for a disc zone. Called during content reads.
|
|
||||||
fn set_read_speed(&mut self, scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()>;
|
fn set_read_speed(&mut self, scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()>;
|
||||||
|
|
||||||
/// True after successful init().
|
|
||||||
fn is_ready(&self) -> bool;
|
fn is_ready(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-308
@@ -1,45 +1,39 @@
|
|||||||
//!
|
//! MT1959 platform — unlock, firmware upload, calibration, speed management.
|
||||||
//! Two variants share this code:
|
|
||||||
//! MT1959-A: mode=0x01, buffer_id=0x44 (all 10 handlers)
|
|
||||||
//! MT1959-B: mode=0x02, buffer_id=0x77 (handlers 4-9, 0-3 are no-ops)
|
|
||||||
//!
|
|
||||||
//! The handler logic is identical across all 206 drives — only the
|
|
||||||
//! profile data differs (signature, register CDBs, microcode, etc).
|
|
||||||
//!
|
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::profile::DriveProfile;
|
use crate::profile::DriveProfile;
|
||||||
use crate::scsi::{self, DataDirection, ScsiTransport};
|
use crate::scsi::{self, DataDirection, ScsiTransport};
|
||||||
use super::Platform;
|
use super::Platform;
|
||||||
|
|
||||||
/// Internal drive status — not exposed to callers.
|
const UNLOCK_RESPONSE_SIZE: u8 = 64;
|
||||||
#[derive(Debug)]
|
|
||||||
struct DriveStatus {
|
// Variant constants
|
||||||
unlocked: bool,
|
const MODE_A: u8 = 0x01;
|
||||||
features: [u8; 16],
|
const MODE_B: u8 = 0x02;
|
||||||
}
|
const BUFFER_ID_A: u8 = 0x44;
|
||||||
|
const BUFFER_ID_B: u8 = 0x77;
|
||||||
|
const NOMINAL_SPEED_A: [u8; 12] = [0xBB, 0x00, 0x23, 0x28, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||||||
|
const NOMINAL_SPEED_B: [u8; 12] = [0x00, 0x00, 0xBB, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00];
|
||||||
|
const FIRMWARE_EXTRA_B: [u8; 16] = [0; 16];
|
||||||
|
const VERIFY_COMMAND_B: [u8; 10] = [0xF1, 0x01, 0x02, 0x00, 0x0D, 0x30, 0x01, 0xF3, 0xAD, 0x23];
|
||||||
|
|
||||||
/// 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: 64 × u16, built by calibrate().
|
|
||||||
/// Stores zone boundary addresses from disc surface probes.
|
|
||||||
speed_table: [u16; 64],
|
speed_table: [u16; 64],
|
||||||
/// Total disc sectors — from READ CAPACITY.
|
|
||||||
disc_sectors: u32,
|
disc_sectors: u32,
|
||||||
calibrated: bool,
|
calibrated: bool,
|
||||||
/// 4 config bytes stored by calibrate() from initial probe response.
|
|
||||||
/// [0]=speed_mult, [1]=data_byte, [2]=data_byte, [3]=last_speed
|
|
||||||
calibration_config: [u8; 4],
|
calibration_config: [u8; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mt1959 {
|
impl Mt1959 {
|
||||||
pub fn new(profile: DriveProfile) -> Self {
|
pub fn new(profile: DriveProfile) -> Self {
|
||||||
let mode = profile.unlock_mode;
|
let (mode, buffer_id) = match profile.variant.as_str() {
|
||||||
let buffer_id = profile.unlock_buf_id;
|
"b" => (MODE_B, BUFFER_ID_B),
|
||||||
|
_ => (MODE_A, BUFFER_ID_A),
|
||||||
|
};
|
||||||
Mt1959 {
|
Mt1959 {
|
||||||
profile,
|
profile,
|
||||||
mode,
|
mode,
|
||||||
@@ -52,8 +46,6 @@ impl Mt1959 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Build READ_BUFFER with sub_cmd in CDB[3] and address in CDB[4:6].
|
|
||||||
fn read_buffer_sub(&self, sub_cmd: u8, address: u16, length: u8) -> [u8; 10] {
|
fn read_buffer_sub(&self, sub_cmd: u8, address: u16, length: u8) -> [u8; 10] {
|
||||||
[
|
[
|
||||||
0x3C, self.mode, self.buffer_id, sub_cmd,
|
0x3C, self.mode, self.buffer_id, sub_cmd,
|
||||||
@@ -62,8 +54,6 @@ impl Mt1959 {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calls dynamic_read_buffer, validates response size matches expected.
|
|
||||||
/// Returns Ok(bytes_transferred) or Err.
|
|
||||||
fn read_buffer_probe(
|
fn read_buffer_probe(
|
||||||
&self, scsi: &mut dyn ScsiTransport,
|
&self, scsi: &mut dyn ScsiTransport,
|
||||||
sub_cmd: u8, address: u16, buf: &mut [u8], expected: usize,
|
sub_cmd: u8, address: u16, buf: &mut [u8], expected: usize,
|
||||||
@@ -76,7 +66,6 @@ impl Mt1959 {
|
|||||||
Ok(result.bytes_transferred)
|
Ok(result.bytes_transferred)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends SET_CD_SPEED from CDB template at 0x9A78: BB 00 FF FF FF FF...
|
|
||||||
fn set_cd_speed_max(&self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
fn set_cd_speed_max(&self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||||
let cdb = scsi::build_set_cd_speed(0xFFFF);
|
let cdb = scsi::build_set_cd_speed(0xFFFF);
|
||||||
let mut dummy = [0u8; 0];
|
let mut dummy = [0u8; 0];
|
||||||
@@ -84,7 +73,6 @@ impl Mt1959 {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build and send a custom SET_CD_SPEED with a specific speed value.
|
|
||||||
fn set_cd_speed(&self, scsi: &mut dyn ScsiTransport, speed: u16) -> Result<()> {
|
fn set_cd_speed(&self, scsi: &mut dyn ScsiTransport, speed: u16) -> Result<()> {
|
||||||
let cdb = scsi::build_set_cd_speed(speed);
|
let cdb = scsi::build_set_cd_speed(speed);
|
||||||
let mut dummy = [0u8; 0];
|
let mut dummy = [0u8; 0];
|
||||||
@@ -92,37 +80,20 @@ impl Mt1959 {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Core unlock function. Returns the full response on success.
|
|
||||||
///
|
|
||||||
/// r3 = unlock_init_value (1)
|
|
||||||
/// sp[0x1C] = r3
|
|
||||||
/// r3 += unlock_response_size_minus_init (0x3F)
|
|
||||||
/// sp[0] = r3 (= 64 = response size)
|
|
||||||
/// scsi_cmd_wrapper(result, 0x0A, unlock_CDB, response)
|
|
||||||
/// check response[0:4] == drive_signature (LE u32)
|
|
||||||
/// check response[12:16] == "MMkv" (0x766B4D4D LE)
|
|
||||||
/// check response[16:20] version range
|
|
||||||
fn do_unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<Vec<u8>> {
|
fn do_unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<Vec<u8>> {
|
||||||
let response_size = self.profile.unlock_init_value as u32
|
|
||||||
+ self.profile.unlock_response_size_minus_init as u32;
|
|
||||||
|
|
||||||
let cdb = [
|
let cdb = [
|
||||||
0x3C, self.mode, self.buffer_id,
|
0x3C, self.mode, self.buffer_id,
|
||||||
0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, response_size as u8, 0x00,
|
0x00, 0x00, UNLOCK_RESPONSE_SIZE, 0x00,
|
||||||
];
|
];
|
||||||
let mut response = vec![0u8; response_size as usize];
|
let mut response = vec![0u8; UNLOCK_RESPONSE_SIZE as usize];
|
||||||
scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?;
|
scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?;
|
||||||
|
|
||||||
if response.len() >= 4 {
|
if response.len() >= 4 && response[0..4] != self.profile.signature {
|
||||||
let got = &response[0..4];
|
return Err(Error::SignatureMismatch {
|
||||||
if got != self.profile.drive_signature {
|
expected: self.profile.signature,
|
||||||
return Err(Error::SignatureMismatch {
|
got: response[0..4].try_into().unwrap_or([0; 4]),
|
||||||
expected: self.profile.drive_signature,
|
});
|
||||||
got: got.try_into().unwrap_or([0; 4]),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.len() >= 16 && &response[12..16] != b"MMkv" {
|
if response.len() >= 16 && &response[12..16] != b"MMkv" {
|
||||||
@@ -134,13 +105,10 @@ impl Mt1959 {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If signature + MMkv match, version is almost always OK.
|
|
||||||
|
|
||||||
self.unlocked = true;
|
self.unlocked = true;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a short READ_BUFFER probe, retries up to 5 times.
|
|
||||||
fn validate(&self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
fn validate(&self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||||
for _attempt in 0..5 {
|
for _attempt in 0..5 {
|
||||||
let cdb = [
|
let cdb = [
|
||||||
@@ -159,7 +127,6 @@ impl Mt1959 {
|
|||||||
|
|
||||||
impl Platform for Mt1959 {
|
impl Platform for Mt1959 {
|
||||||
fn init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
fn init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||||
// Guard: don't re-init an already-ready drive
|
|
||||||
if self.unlocked && self.calibrated {
|
if self.unlocked && self.calibrated {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -178,116 +145,42 @@ impl Platform for Mt1959 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── All handlers are PRIVATE — only callable through init() ────────────
|
|
||||||
|
|
||||||
impl Mt1959 {
|
impl Mt1959 {
|
||||||
fn unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
fn unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||||
self.do_unlock(scsi)?;
|
self.do_unlock(scsi)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
/// Two variants with different upload sequences:
|
|
||||||
///
|
|
||||||
/// 1. WRITE_BUFFER mode=6 with ld_microcode
|
|
||||||
/// 2. READ_BUFFER buf=0x45 verify (expect response == 2)
|
|
||||||
/// 3. do_unlock() × 2
|
|
||||||
///
|
|
||||||
/// 1. WRITE_BUFFER with mode=2 buf=0x77 initial handshake (0x9C0 bytes)
|
|
||||||
/// 2. Check response == 2
|
|
||||||
/// 3. READ_BUFFER at offset 0x3000 (16 bytes, firmware metadata check)
|
|
||||||
/// 4. WRITE_BUFFER mode=6 with ld_microcode (16 bytes from payload+16)
|
|
||||||
/// 5. READ verify
|
|
||||||
/// 6. do_unlock() × 5 retries
|
|
||||||
fn load_firmware(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
fn load_firmware(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||||
let microcode = &self.profile.ld_microcode;
|
if self.profile.firmware.is_empty() {
|
||||||
if microcode.is_empty() {
|
|
||||||
return Err(Error::UnlockFailed {
|
return Err(Error::UnlockFailed {
|
||||||
detail: "no ld_microcode in profile".into(),
|
detail: "no firmware in profile".into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.mode == 0x01 {
|
if self.mode == MODE_A {
|
||||||
// ── MT1959-A path ──────────────────────────────────────────
|
self.load_firmware_a(scsi)
|
||||||
self.load_firmware_a(scsi)?;
|
|
||||||
} else {
|
} else {
|
||||||
// ── MT1959-B path ──────────────────────────────────────────
|
self.load_firmware_b(scsi)
|
||||||
self.load_firmware_b(scsi)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
/// do_unlock → validate × 5 → send hardware_register_a_cdb (10B) →
|
|
||||||
/// check 36B response → return response[4:20] (16 bytes)
|
|
||||||
fn read_register_a(&mut self, scsi: &mut dyn ScsiTransport) -> Result<[u8; 16]> {
|
|
||||||
if !self.unlocked { self.do_unlock(scsi)?; }
|
|
||||||
self.validate(scsi)?;
|
|
||||||
|
|
||||||
let cdb = &self.profile.hardware_register_a_cdb;
|
|
||||||
if cdb.len() != 10 {
|
|
||||||
return Err(Error::UnlockFailed { detail: "missing register_a_cdb".into() });
|
|
||||||
}
|
|
||||||
let mut response = [0u8; 36];
|
|
||||||
scsi.execute(cdb, DataDirection::FromDevice, &mut response, 30_000)?;
|
|
||||||
|
|
||||||
let mut out = [0u8; 16];
|
|
||||||
out.copy_from_slice(&response[4..20]);
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_register_b(&mut self, scsi: &mut dyn ScsiTransport) -> Result<[u8; 16]> {
|
|
||||||
if !self.unlocked { self.do_unlock(scsi)?; }
|
|
||||||
self.validate(scsi)?;
|
|
||||||
|
|
||||||
let cdb = &self.profile.hardware_register_b_cdb;
|
|
||||||
if cdb.len() != 10 {
|
|
||||||
return Err(Error::UnlockFailed { detail: "missing register_b_cdb".into() });
|
|
||||||
}
|
|
||||||
let mut response = [0u8; 36];
|
|
||||||
scsi.execute(cdb, DataDirection::FromDevice, &mut response, 30_000)?;
|
|
||||||
|
|
||||||
let mut out = [0u8; 16];
|
|
||||||
out.copy_from_slice(&response[4..20]);
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Full calibration sequence:
|
|
||||||
/// 1. do_unlock()
|
|
||||||
/// 2. init_timing()
|
|
||||||
/// 3. read_buffer_probe(0x12, init_addr, 4) — calibration init
|
|
||||||
/// 4. validate_with_retry()
|
|
||||||
/// 5. memset speed_table to 0
|
|
||||||
/// 6. First probe: sub_cmd=0x14, addr=0 → initial speed
|
|
||||||
/// 7. Scan loop: addresses 0x0000-0x5800, step 0x100, detect zone boundaries
|
|
||||||
/// 8. Build loop: scan all zones up to 0x10000, store in speed_table[(speed>>1)-1]
|
|
||||||
/// 9. Triple SET_CD_SPEED: max → drive_nominal_speed → max
|
|
||||||
/// 10. Store 4 calibration config bytes
|
|
||||||
fn calibrate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
fn calibrate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||||
// Step 1: ensure unlocked
|
|
||||||
if !self.unlocked { self.do_unlock(scsi)?; }
|
if !self.unlocked { self.do_unlock(scsi)?; }
|
||||||
|
|
||||||
// Step 2: read disc capacity
|
|
||||||
let cap_cdb = [0x25u8, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
let cap_cdb = [0x25u8, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
let mut cap_buf = [0u8; 8];
|
let mut cap_buf = [0u8; 8];
|
||||||
if scsi.execute(&cap_cdb, DataDirection::FromDevice, &mut cap_buf, 5_000).is_ok() {
|
if scsi.execute(&cap_cdb, DataDirection::FromDevice, &mut cap_buf, 5_000).is_ok() {
|
||||||
self.disc_sectors = u32::from_be_bytes([cap_buf[0], cap_buf[1], cap_buf[2], cap_buf[3]]) + 1;
|
self.disc_sectors = u32::from_be_bytes([cap_buf[0], cap_buf[1], cap_buf[2], cap_buf[3]]) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: calibration init — sub_cmd 0x12
|
let init_addr: u16 = 0x0100; // TODO: detect disc type (0x0200 for UHD)
|
||||||
let init_addr: u16 = 0x0100; // TODO: detect disc type
|
|
||||||
let mut init_resp = [0u8; 4];
|
let mut init_resp = [0u8; 4];
|
||||||
let _ = self.read_buffer_probe(scsi, 0x12, init_addr, &mut init_resp, 4);
|
let _ = self.read_buffer_probe(scsi, 0x12, init_addr, &mut init_resp, 4);
|
||||||
|
|
||||||
// Step 4: validate
|
|
||||||
self.validate(scsi)?;
|
self.validate(scsi)?;
|
||||||
|
|
||||||
// Step 5: clear speed table
|
|
||||||
self.speed_table = [0u16; 64];
|
self.speed_table = [0u16; 64];
|
||||||
|
|
||||||
// Step 6: first probe
|
|
||||||
let mut probe_buf = [0u8; 4];
|
let mut probe_buf = [0u8; 4];
|
||||||
let _ = self.read_buffer_probe(scsi, 0x14, 0, &mut probe_buf, 4);
|
let _ = self.read_buffer_probe(scsi, 0x14, 0, &mut probe_buf, 4);
|
||||||
let initial_speed = probe_buf[0];
|
let initial_speed = probe_buf[0];
|
||||||
@@ -295,7 +188,6 @@ impl Mt1959 {
|
|||||||
self.calibration_config[1] = probe_buf[1];
|
self.calibration_config[1] = probe_buf[1];
|
||||||
self.calibration_config[2] = probe_buf[2];
|
self.calibration_config[2] = probe_buf[2];
|
||||||
|
|
||||||
// Step 7: scan loop — find zone boundaries (0x0000-0x5800, step 0x100)
|
|
||||||
let mut addr: u16 = 0;
|
let mut addr: u16 = 0;
|
||||||
let mut prev_speed = initial_speed;
|
let mut prev_speed = initial_speed;
|
||||||
while addr < 0x5800 {
|
while addr < 0x5800 {
|
||||||
@@ -311,7 +203,6 @@ impl Mt1959 {
|
|||||||
addr = addr.wrapping_add(0x100);
|
addr = addr.wrapping_add(0x100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 8: build speed table — probe all zones up to 0x10000
|
|
||||||
let mut addr: u32 = 0;
|
let mut addr: u32 = 0;
|
||||||
let mut prev_speed: u8 = 0;
|
let mut prev_speed: u8 = 0;
|
||||||
while addr < 0x10000 {
|
while addr < 0x10000 {
|
||||||
@@ -331,114 +222,23 @@ impl Mt1959 {
|
|||||||
}
|
}
|
||||||
self.calibration_config[3] = prev_speed;
|
self.calibration_config[3] = prev_speed;
|
||||||
|
|
||||||
// Step 9: triple SET_CD_SPEED — max → nominal → max
|
|
||||||
let _ = self.set_cd_speed_max(scsi);
|
let _ = self.set_cd_speed_max(scsi);
|
||||||
|
let nominal = if self.mode == MODE_A { &NOMINAL_SPEED_A } else { &NOMINAL_SPEED_B };
|
||||||
// Send drive_nominal_speed_cdb (the specific speed from the profile)
|
let mut dummy = [0u8; 0];
|
||||||
if self.profile.drive_nominal_speed_cdb.len() >= 6 {
|
let _ = scsi.execute(nominal, DataDirection::None, &mut dummy, 5_000);
|
||||||
let cdb = &self.profile.drive_nominal_speed_cdb;
|
|
||||||
let mut dummy = [0u8; 0];
|
|
||||||
let _ = scsi.execute(cdb, DataDirection::None, &mut dummy, 5_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = self.set_cd_speed_max(scsi);
|
let _ = self.set_cd_speed_max(scsi);
|
||||||
|
|
||||||
self.calibrated = true;
|
self.calibrated = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
/// NOT a no-op. Sends 16 bytes from a data buffer to the host via host_write.
|
|
||||||
/// In our context: the x86 reads 16 bytes back. We return the data.
|
|
||||||
/// For now we just acknowledge — the x86 calls this to confirm VM is alive.
|
|
||||||
fn keepalive(&mut self, _scsi: &mut dyn ScsiTransport) -> Result<()> {
|
|
||||||
// In driver context: no host_write mechanism. This is a VM-to-host
|
|
||||||
// communication that doesn't translate to a SCSI command.
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Full status check:
|
|
||||||
/// 1. Read 4 bytes from host (param input)
|
|
||||||
/// 2. Validate param == 4
|
|
||||||
/// 3. do_unlock()
|
|
||||||
/// 4. validate_with_retry()
|
|
||||||
/// 6. read_buffer_probe(0x13, addr, response, 36)
|
|
||||||
/// 7. validate_with_retry() again
|
|
||||||
/// 8. Check REV32(response[0:4]) == 0x00220054
|
|
||||||
/// 9. If mismatch: call fallback() with speed_zone_table
|
|
||||||
/// 10. If match: host_write(response[4:20]) — 16 bytes features
|
|
||||||
fn status(&mut self, scsi: &mut dyn ScsiTransport) -> Result<DriveStatus> {
|
|
||||||
if !self.unlocked { self.do_unlock(scsi)?; }
|
|
||||||
self.validate(scsi)?;
|
|
||||||
|
|
||||||
let cdb = self.read_buffer_sub(0x13, 0, 36);
|
|
||||||
let mut response = [0u8; 36];
|
|
||||||
scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?;
|
|
||||||
|
|
||||||
self.validate(scsi)?;
|
|
||||||
|
|
||||||
let sig = u32::from_be_bytes(response[0..4].try_into().unwrap());
|
|
||||||
let expected = 0x00220054;
|
|
||||||
|
|
||||||
let mut features = [0u8; 16];
|
|
||||||
features.copy_from_slice(&response[4..20]);
|
|
||||||
|
|
||||||
Ok(DriveStatus {
|
|
||||||
unlocked: sig == expected,
|
|
||||||
features,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Three code paths based on param count:
|
|
||||||
/// param=1 (5 bytes in): sub_cmd + nothing else → dynamic_read_buffer
|
|
||||||
/// param=5 (5+4 bytes in): sub_cmd + address → dynamic_read_buffer with addr
|
|
||||||
/// param=9 (9+ bytes in): builds full 12-byte CDB on stack:
|
|
||||||
/// [0x3C, mode, buf_id, sub_cmd, addr[2], addr[1], addr[0], len[2], len[1], len[0]]
|
|
||||||
/// then calls scsi_cmd_wrapper directly
|
|
||||||
fn probe(
|
|
||||||
&mut self, scsi: &mut dyn ScsiTransport,
|
|
||||||
sub_cmd: u8, address: u32, length: u32,
|
|
||||||
) -> Result<Vec<u8>> {
|
|
||||||
let cdb = [
|
|
||||||
0x3C, self.mode, self.buffer_id, sub_cmd,
|
|
||||||
(address >> 16) as u8, (address >> 8) as u8, address as u8,
|
|
||||||
(length >> 16) as u8, (length >> 8) as u8, length as u8,
|
|
||||||
];
|
|
||||||
let mut buf = vec![0u8; length as usize];
|
|
||||||
let result = scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 30_000)?;
|
|
||||||
buf.truncate(result.bytes_transferred);
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Speed management for content reads. Called per zone change.
|
|
||||||
///
|
|
||||||
/// 1. Read 4-byte target LBA from host
|
|
||||||
/// 2. REV(LBA) for big-endian comparison
|
|
||||||
/// 3. Search speed_table[64] for closest entry to LBA
|
|
||||||
/// - Each entry is u16 zone boundary address
|
|
||||||
/// - Find entry with smallest |entry - LBA| distance
|
|
||||||
/// 4. If no match found → call fallback() with speed_calc_table
|
|
||||||
/// 5. If match:
|
|
||||||
/// a. r6 = speed_table[best_idx] (the zone address = speed value)
|
|
||||||
/// b. Position check: read_buffer_probe(0x14, 0x100 | rev16(r6), 4)
|
|
||||||
/// c. SET_CD_SPEED max (BB 00 FF FF FF FF)
|
|
||||||
/// d. Build custom SET_CD_SPEED: BB 00 [r6>>8] [r6&FF] FF FF 00...
|
|
||||||
/// e. Send custom SET_CD_SPEED
|
|
||||||
/// f. Return next speed_table entry to host
|
|
||||||
fn run_set_read_speed(&mut self, scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()> {
|
fn run_set_read_speed(&mut self, scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()> {
|
||||||
if !self.calibrated {
|
if !self.calibrated {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// the byte-swapped LBA against table entries. Since entries are
|
|
||||||
// zone boundary addresses (u16), we compare the low 16 bits.
|
|
||||||
|
|
||||||
// Step 2-3: search speed_table for closest entry
|
|
||||||
let mut best_idx: usize = 0;
|
let mut best_idx: usize = 0;
|
||||||
let mut best_diff: u32 = 0x10000000;
|
let mut best_diff: u32 = 0x10000000;
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
|
|
||||||
for i in 0..64 {
|
for i in 0..64 {
|
||||||
@@ -453,43 +253,21 @@ impl Mt1959 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
// For now, skip speed adjustment when no table match
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5a: get the matched speed value
|
|
||||||
let speed_val = self.speed_table[best_idx];
|
let speed_val = self.speed_table[best_idx];
|
||||||
|
|
||||||
// Step 5b: position check probe
|
|
||||||
let probe_addr = 0x0100 | (speed_val.swap_bytes() as u16);
|
let probe_addr = 0x0100 | (speed_val.swap_bytes() as u16);
|
||||||
let mut probe_resp = [0u8; 4];
|
let mut probe_resp = [0u8; 4];
|
||||||
let _ = self.read_buffer_probe(scsi, 0x14, probe_addr, &mut probe_resp, 4);
|
let _ = self.read_buffer_probe(scsi, 0x14, probe_addr, &mut probe_resp, 4);
|
||||||
|
|
||||||
// Step 5c: SET_CD_SPEED max
|
|
||||||
let _ = self.set_cd_speed_max(scsi);
|
let _ = self.set_cd_speed_max(scsi);
|
||||||
|
|
||||||
// Step 5d-e: custom SET_CD_SPEED with the matched speed value
|
|
||||||
let _ = self.set_cd_speed(scsi, speed_val);
|
let _ = self.set_cd_speed(scsi, speed_val);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
/// Reads 8 bytes from host, byte-swaps as 32-bit value, returns.
|
|
||||||
/// Has a helper for unaligned 4-byte reads with manual byte copy.
|
|
||||||
/// In driver context: no host communication, so this is a no-op.
|
|
||||||
fn timing(&mut self, _scsi: &mut dyn ScsiTransport) -> Result<()> {
|
|
||||||
// VM-to-host timing measurement, not a SCSI command.
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Full init sequence — matches x86 dispatch exactly.
|
|
||||||
fn run_init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
fn run_init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||||
// Phase 1: Unlock + firmware upload (6 retries)
|
|
||||||
//
|
|
||||||
// Three unlock outcomes:
|
|
||||||
// Ok → warm drive, firmware loaded, skip to calibrate
|
|
||||||
// Err(other) → cold drive or SCSI error, try load_firmware
|
|
||||||
let mut unlocked = false;
|
let mut unlocked = false;
|
||||||
for _attempt in 0..6 {
|
for _attempt in 0..6 {
|
||||||
match self.unlock(scsi) {
|
match self.unlock(scsi) {
|
||||||
@@ -500,7 +278,6 @@ impl Mt1959 {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Cold boot or SCSI error — try uploading firmware
|
|
||||||
if self.load_firmware(scsi).is_ok() {
|
if self.load_firmware(scsi).is_ok() {
|
||||||
unlocked = true;
|
unlocked = true;
|
||||||
break;
|
break;
|
||||||
@@ -510,11 +287,10 @@ impl Mt1959 {
|
|||||||
}
|
}
|
||||||
if !unlocked {
|
if !unlocked {
|
||||||
return Err(Error::UnlockFailed {
|
return Err(Error::UnlockFailed {
|
||||||
detail: "failed after 6 attempts (unlock + load_firmware)".into(),
|
detail: "failed after 6 attempts".into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: Calibrate (6 retries)
|
|
||||||
let mut calibrated = false;
|
let mut calibrated = false;
|
||||||
for _attempt in 0..6 {
|
for _attempt in 0..6 {
|
||||||
if self.calibrate(scsi).is_ok() {
|
if self.calibrate(scsi).is_ok() {
|
||||||
@@ -526,95 +302,57 @@ impl Mt1959 {
|
|||||||
return Err(Error::ScsiError { opcode: 0x3C, status: 0xFF, sense_key: 0 });
|
return Err(Error::ScsiError { opcode: 0x3C, status: 0xFF, sense_key: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3: Read registers — non-fatal
|
|
||||||
// x86 retries 5×, but we do a single attempt each. Not required for reads.
|
|
||||||
let _ = self.read_register_a(scsi);
|
|
||||||
let _ = self.read_register_b(scsi);
|
|
||||||
|
|
||||||
// Phase 4: Status — non-fatal, single attempt
|
|
||||||
// Some drives reject sub_cmd 0x13. Not required for reads.
|
|
||||||
let _ = self.status(scsi);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Private firmware upload variants ───────────────────────────────────
|
|
||||||
|
|
||||||
impl Mt1959 {
|
impl Mt1959 {
|
||||||
///
|
|
||||||
/// Simple: WRITE all microcode → verify buf=0x45 → unlock × 2.
|
|
||||||
fn load_firmware_a(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
fn load_firmware_a(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||||
let microcode = &self.profile.ld_microcode;
|
let firmware = &self.profile.firmware;
|
||||||
let len = microcode.len();
|
let len = firmware.len();
|
||||||
|
|
||||||
// WRITE_BUFFER mode=6: send entire microcode payload
|
|
||||||
let cdb = [
|
let cdb = [
|
||||||
0x3B, 0x06, 0x00,
|
0x3B, 0x06, 0x00,
|
||||||
0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00,
|
||||||
(len >> 16) as u8, (len >> 8) as u8, len as u8,
|
(len >> 16) as u8, (len >> 8) as u8, len as u8,
|
||||||
0x00,
|
0x00,
|
||||||
];
|
];
|
||||||
let mut data = microcode.clone();
|
let mut data = firmware.clone();
|
||||||
scsi.execute(&cdb, DataDirection::ToDevice, &mut data, 30_000)?;
|
scsi.execute(&cdb, DataDirection::ToDevice, &mut data, 30_000)?;
|
||||||
|
|
||||||
let verify_cdb = [0x3C, 0x01, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00];
|
let verify_cdb = [0x3C, 0x01, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00];
|
||||||
let mut verify_resp = [0u8; 4];
|
let mut verify_resp = [0u8; 4];
|
||||||
let _ = scsi.execute(
|
let _ = scsi.execute(&verify_cdb, DataDirection::FromDevice, &mut verify_resp, 5_000);
|
||||||
&verify_cdb, DataDirection::FromDevice, &mut verify_resp, 5_000,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Unlock × 2
|
|
||||||
self.do_unlock(scsi)?;
|
self.do_unlock(scsi)?;
|
||||||
self.do_unlock(scsi)?;
|
self.do_unlock(scsi)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
/// 1. MODE SELECT (0x55) — send 2496 bytes from ld_microcode
|
|
||||||
/// 2. Check result == 2 (firmware accepted)
|
|
||||||
/// 3. READ_BUFFER mode=6 offset=0x3000 (16 bytes firmware metadata)
|
|
||||||
/// 4. WRITE_BUFFER mode=6 (16 bytes from profile's fw_write_data)
|
|
||||||
/// 5. Vendor verify command (from profile's verify_cdb)
|
|
||||||
/// 6. do_unlock() × 5 retries + 1 final
|
|
||||||
fn load_firmware_b(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
fn load_firmware_b(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||||
let microcode = &self.profile.ld_microcode;
|
let firmware = &self.profile.firmware;
|
||||||
|
|
||||||
// Step 1: MODE SELECT (0x55) with vendor mode page data
|
let write_len = 0x9C0usize.min(firmware.len());
|
||||||
// Transfer: 0x9C0 = 2496 bytes from the firmware payload
|
|
||||||
let write_len = 0x9C0usize.min(microcode.len());
|
|
||||||
let mode_select_cdb = [
|
let mode_select_cdb = [
|
||||||
0x55, 0x10, 0x00,
|
0x55, 0x10, 0x00,
|
||||||
0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00,
|
||||||
(write_len >> 16) as u8, (write_len >> 8) as u8, write_len as u8,
|
(write_len >> 16) as u8, (write_len >> 8) as u8, write_len as u8,
|
||||||
0x00,
|
0x00,
|
||||||
];
|
];
|
||||||
let mut data = microcode[..write_len].to_vec();
|
let mut data = firmware[..write_len].to_vec();
|
||||||
scsi.execute(&mode_select_cdb, DataDirection::ToDevice, &mut data, 30_000)?;
|
scsi.execute(&mode_select_cdb, DataDirection::ToDevice, &mut data, 30_000)?;
|
||||||
|
|
||||||
// The execute above will Err on SCSI failure. If we get here, command accepted.
|
|
||||||
|
|
||||||
// Step 3: READ_BUFFER mode=6, offset=0x3000, 16 bytes
|
|
||||||
let read_meta_cdb = [0x3C, 0x06, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00];
|
let read_meta_cdb = [0x3C, 0x06, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00];
|
||||||
let mut meta_resp = [0u8; 16];
|
let mut meta_resp = [0u8; 16];
|
||||||
let _ = scsi.execute(&read_meta_cdb, DataDirection::FromDevice, &mut meta_resp, 5_000);
|
let _ = scsi.execute(&read_meta_cdb, DataDirection::FromDevice, &mut meta_resp, 5_000);
|
||||||
|
|
||||||
// Step 4: WRITE_BUFFER mode=6, 16 bytes
|
let write2_cdb = [0x3B, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00];
|
||||||
// This data is stored in profile.fw_write_data (16 bytes)
|
let mut data2 = FIRMWARE_EXTRA_B.to_vec();
|
||||||
if self.profile.fw_write_data.len() >= 16 {
|
let _ = scsi.execute(&write2_cdb, DataDirection::ToDevice, &mut data2, 5_000);
|
||||||
let write2_cdb = [0x3B, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00];
|
|
||||||
let mut data2 = self.profile.fw_write_data[..16].to_vec();
|
|
||||||
let _ = scsi.execute(&write2_cdb, DataDirection::ToDevice, &mut data2, 5_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: Vendor verify command
|
let mut dummy = [0u8; 0];
|
||||||
if self.profile.verify_cdb.len() >= 10 {
|
let _ = scsi.execute(&VERIFY_COMMAND_B, DataDirection::None, &mut dummy, 5_000);
|
||||||
let mut dummy = [0u8; 0];
|
|
||||||
let _ = scsi.execute(&self.profile.verify_cdb, DataDirection::None, &mut dummy, 5_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 6: do_unlock() × 5 retries + 1 confirmation
|
|
||||||
for _attempt in 0..5 {
|
for _attempt in 0..5 {
|
||||||
if self.do_unlock(scsi).is_ok() {
|
if self.do_unlock(scsi).is_ok() {
|
||||||
let _ = self.do_unlock(scsi);
|
let _ = self.do_unlock(scsi);
|
||||||
|
|||||||
+62
-161
@@ -1,151 +1,63 @@
|
|||||||
//! Drive profile loading and matching.
|
//! Drive profile loading and matching.
|
||||||
//!
|
|
||||||
//! The profile contains all per-drive data needed by the MT1959 platform handlers.
|
|
||||||
//! Profiles are loaded from JSON so new drives can be added without rebuilding.
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
///
|
/// Top-level profiles file — keyed by chipset.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct DriveProfile {
|
pub struct ProfilesFile {
|
||||||
// ── Drive identity (from INQUIRY + GET_CONFIG) ─────────────────────
|
#[serde(default)]
|
||||||
|
pub mt1959: Vec<DriveProfile>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub renesas: Vec<DriveProfile>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Drive vendor from INQUIRY[8:16] (e.g. "HL-DT-ST")
|
/// Drive identity — matched against INQUIRY data.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct Identity {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub vendor_id: String,
|
pub vendor_id: String,
|
||||||
|
|
||||||
/// Drive product from INQUIRY[16:32] (e.g. "BD-RE BU40N")
|
|
||||||
#[serde(default)]
|
|
||||||
pub product_id: String,
|
|
||||||
|
|
||||||
/// Firmware revision from INQUIRY[32:36] (e.g. "1.03")
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub product_revision: String,
|
pub product_revision: String,
|
||||||
|
|
||||||
/// Firmware type from INQUIRY[36:43] (e.g. "NM00000")
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub vendor_specific: String,
|
pub vendor_specific: String,
|
||||||
|
|
||||||
/// Firmware build date from GET_CONFIG 010C (e.g. "211810241934")
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub firmware_date: String,
|
pub firmware_date: String,
|
||||||
|
}
|
||||||
|
|
||||||
// ── Platform variant ───────────────────────────────────────────────
|
/// Per-drive profile.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
/// Chipset family: "mediatek" or "renesas".
|
pub struct DriveProfile {
|
||||||
|
pub identity: Identity,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub chipset: Chipset,
|
pub variant: String,
|
||||||
|
|
||||||
/// Program variant: "mt1959_a" or "mt1959_b".
|
|
||||||
/// Determines unlock mode/buf_id and handler layout.
|
|
||||||
#[serde(default)]
|
|
||||||
pub program: String,
|
|
||||||
|
|
||||||
/// READ_BUFFER mode byte (0x01 for mt1959_a, 0x02 for mt1959_b).
|
|
||||||
#[serde(default = "default_unlock_mode")]
|
|
||||||
pub unlock_mode: u8,
|
|
||||||
|
|
||||||
/// READ_BUFFER buffer ID (0x44 for mt1959_a, 0x77 for mt1959_b).
|
|
||||||
#[serde(default = "default_unlock_buf_id")]
|
|
||||||
pub unlock_buf_id: u8,
|
|
||||||
|
|
||||||
|
|
||||||
/// Used with unlock_response_size_minus_init to compute response size.
|
|
||||||
#[serde(default = "default_init_value")]
|
|
||||||
pub unlock_init_value: u8,
|
|
||||||
|
|
||||||
/// Response size = unlock_init_value + this (e.g. 1 + 63 = 64).
|
|
||||||
#[serde(default = "default_response_size_minus_init")]
|
|
||||||
pub unlock_response_size_minus_init: u8,
|
|
||||||
|
|
||||||
/// Per-drive signature checked against unlock response[0:4].
|
|
||||||
#[serde(default, deserialize_with = "deserialize_hex4")]
|
#[serde(default, deserialize_with = "deserialize_hex4")]
|
||||||
pub drive_signature: [u8; 4],
|
pub signature: [u8; 4],
|
||||||
|
|
||||||
|
|
||||||
/// Uploaded on cold boot when unlock fails. ~1888 bytes typically.
|
|
||||||
/// Contains volatile RAM-only runtime code for the drive's MediaTek SOC.
|
|
||||||
#[serde(default, deserialize_with = "deserialize_base64")]
|
#[serde(default, deserialize_with = "deserialize_base64")]
|
||||||
pub ld_microcode: Vec<u8>,
|
pub firmware: Vec<u8>,
|
||||||
|
|
||||||
// ── Handlers 2/3: register reads ──────────────────────────────────
|
|
||||||
|
|
||||||
#[serde(default, deserialize_with = "deserialize_hex_vec")]
|
|
||||||
pub hardware_register_a_cdb: Vec<u8>,
|
|
||||||
|
|
||||||
#[serde(default, deserialize_with = "deserialize_hex_vec")]
|
|
||||||
pub hardware_register_b_cdb: Vec<u8>,
|
|
||||||
|
|
||||||
|
|
||||||
/// 16 bytes written via WRITE_BUFFER mode=6 during B firmware upload.
|
|
||||||
#[serde(default, deserialize_with = "deserialize_hex_vec")]
|
|
||||||
pub fw_write_data: Vec<u8>,
|
|
||||||
|
|
||||||
/// Vendor-specific verify CDB after B firmware upload (10 bytes).
|
|
||||||
/// B-only. Empty for A-variant drives.
|
|
||||||
#[serde(default, deserialize_with = "deserialize_hex_vec")]
|
|
||||||
pub verify_cdb: Vec<u8>,
|
|
||||||
|
|
||||||
|
|
||||||
/// Pre-built SET_CD_SPEED CDB with drive's nominal speed (12 bytes).
|
|
||||||
/// Used in calibration "triple play": max → this → max.
|
|
||||||
#[serde(default, deserialize_with = "deserialize_hex_vec")]
|
|
||||||
pub drive_nominal_speed_cdb: Vec<u8>,
|
|
||||||
|
|
||||||
|
|
||||||
/// for per-zone speed decisions. Per-drive calibration constants.
|
|
||||||
#[serde(default, deserialize_with = "deserialize_hex_vec")]
|
|
||||||
pub speed_zone_table: Vec<u8>,
|
|
||||||
|
|
||||||
/// raw sector reads for speed math.
|
|
||||||
#[serde(default, deserialize_with = "deserialize_hex_vec")]
|
|
||||||
pub speed_calc_table: Vec<u8>,
|
|
||||||
|
|
||||||
|
|
||||||
/// Drive supports reading DVDs regardless of region code.
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dvd_all_regions: bool,
|
pub dvd_all_regions: bool,
|
||||||
|
|
||||||
/// Drive supports raw Blu-ray sector reads.
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub bd_raw_read: bool,
|
pub bd_raw_read: bool,
|
||||||
|
|
||||||
/// Drive supports raw Blu-ray metadata reads.
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub bd_raw_metadata: bool,
|
pub bd_raw_metadata: bool,
|
||||||
|
|
||||||
/// Drive supports unrestricted read speed.
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub unrestricted_speed: bool,
|
pub unrestricted_speed: bool,
|
||||||
|
|
||||||
// ── Metadata ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub drive_id: String,
|
pub drive_id: String,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub drive_version: String,
|
pub drive_version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_unlock_mode() -> u8 { 0x01 }
|
/// Chipset — determined by which section the profile was found in.
|
||||||
fn default_unlock_buf_id() -> u8 { 0x44 }
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
fn default_init_value() -> u8 { 1 }
|
|
||||||
fn default_response_size_minus_init() -> u8 { 0x3F }
|
|
||||||
|
|
||||||
/// Drive chipset family.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
|
|
||||||
pub enum Chipset {
|
pub enum Chipset {
|
||||||
#[serde(rename = "mediatek")]
|
|
||||||
MediaTek,
|
MediaTek,
|
||||||
#[serde(rename = "renesas")]
|
|
||||||
Renesas,
|
Renesas,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Chipset {
|
|
||||||
fn default() -> Self { Chipset::MediaTek }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Chipset {
|
impl Chipset {
|
||||||
pub fn name(&self) -> &'static str {
|
pub fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
@@ -155,6 +67,12 @@ impl Chipset {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Result of profile lookup — includes chipset from the section it was found in.
|
||||||
|
pub struct ProfileMatch {
|
||||||
|
pub profile: DriveProfile,
|
||||||
|
pub chipset: Chipset,
|
||||||
|
}
|
||||||
|
|
||||||
// ── Hex/base64 parsing ─────────────────────────────────────────────────
|
// ── Hex/base64 parsing ─────────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse_hex4(s: &str) -> Result<[u8; 4]> {
|
fn parse_hex4(s: &str) -> Result<[u8; 4]> {
|
||||||
@@ -169,18 +87,6 @@ fn parse_hex4(s: &str) -> Result<[u8; 4]> {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_hex(s: &str) -> Result<Vec<u8>> {
|
|
||||||
if s.len() % 2 != 0 {
|
|
||||||
return Err(Error::ProfileParse { detail: "odd hex length".into() });
|
|
||||||
}
|
|
||||||
let mut out = Vec::with_capacity(s.len() / 2);
|
|
||||||
for i in (0..s.len()).step_by(2) {
|
|
||||||
out.push(u8::from_str_radix(&s[i..i+2], 16)
|
|
||||||
.map_err(|e| Error::ProfileParse { detail: format!("bad hex: {e}") })?);
|
|
||||||
}
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_hex4<'de, D>(deserializer: D) -> std::result::Result<[u8; 4], D::Error>
|
fn deserialize_hex4<'de, D>(deserializer: D) -> std::result::Result<[u8; 4], D::Error>
|
||||||
where D: serde::Deserializer<'de> {
|
where D: serde::Deserializer<'de> {
|
||||||
let s = String::deserialize(deserializer)?;
|
let s = String::deserialize(deserializer)?;
|
||||||
@@ -188,13 +94,6 @@ where D: serde::Deserializer<'de> {
|
|||||||
parse_hex4(&s).map_err(serde::de::Error::custom)
|
parse_hex4(&s).map_err(serde::de::Error::custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_hex_vec<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
|
|
||||||
where D: serde::Deserializer<'de> {
|
|
||||||
let s = String::deserialize(deserializer)?;
|
|
||||||
if s.is_empty() { return Ok(Vec::new()); }
|
|
||||||
parse_hex(&s).map_err(serde::de::Error::custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_base64<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
|
fn deserialize_base64<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
|
||||||
where D: serde::Deserializer<'de> {
|
where D: serde::Deserializer<'de> {
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
@@ -207,48 +106,50 @@ where D: serde::Deserializer<'de> {
|
|||||||
|
|
||||||
// ── Loading ────────────────────────────────────────────────────────────
|
// ── Loading ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Bundled profiles — compiled into the binary.
|
|
||||||
const BUNDLED_PROFILES: &str = include_str!("../profiles.json");
|
const BUNDLED_PROFILES: &str = include_str!("../profiles.json");
|
||||||
|
|
||||||
/// Load profiles from the bundled database.
|
pub fn load_bundled() -> Result<ProfilesFile> {
|
||||||
pub fn load_bundled() -> Result<Vec<DriveProfile>> {
|
|
||||||
load_from_str(BUNDLED_PROFILES)
|
load_from_str(BUNDLED_PROFILES)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load all profiles from a JSON array file.
|
fn load_from_str(data: &str) -> Result<ProfilesFile> {
|
||||||
pub fn load_all(path: &std::path::Path) -> Result<Vec<DriveProfile>> {
|
serde_json::from_str(data)
|
||||||
let data = std::fs::read_to_string(path)?;
|
.map_err(|e| Error::ProfileParse { detail: format!("JSON: {e}") })
|
||||||
load_from_str(&data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_from_str(data: &str) -> Result<Vec<DriveProfile>> {
|
/// Find a profile matching a drive's INQUIRY fields. Returns profile + chipset.
|
||||||
let arr: Vec<DriveProfile> = serde_json::from_str(data)
|
pub fn find_by_drive_id(
|
||||||
.map_err(|e| Error::ProfileParse { detail: format!("JSON: {e}") })?;
|
profiles: &ProfilesFile,
|
||||||
Ok(arr)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find a profile matching a drive's INQUIRY fields.
|
|
||||||
pub fn find_by_drive_id<'a>(
|
|
||||||
profiles: &'a [DriveProfile],
|
|
||||||
drive_id: &crate::identity::DriveId,
|
drive_id: &crate::identity::DriveId,
|
||||||
) -> Option<&'a DriveProfile> {
|
) -> Option<ProfileMatch> {
|
||||||
let v = drive_id.vendor_id.trim();
|
let v = drive_id.vendor_id.trim();
|
||||||
let r = drive_id.product_revision.trim();
|
let r = drive_id.product_revision.trim();
|
||||||
let vs = drive_id.vendor_specific.trim();
|
let vs = drive_id.vendor_specific.trim();
|
||||||
|
let date = drive_id.firmware_date.trim();
|
||||||
|
|
||||||
// Match all four INQUIRY fields
|
for (chipset, list) in [
|
||||||
profiles.iter().find(|p| {
|
(Chipset::MediaTek, &profiles.mt1959),
|
||||||
p.vendor_id.trim() == v
|
(Chipset::Renesas, &profiles.renesas),
|
||||||
&& p.product_revision.trim() == r
|
] {
|
||||||
&& p.vendor_specific.trim() == vs
|
if let Some(p) = list.iter().find(|p| {
|
||||||
&& p.firmware_date.trim() == drive_id.firmware_date.trim()
|
p.identity.vendor_id.trim() == v
|
||||||
})
|
&& p.identity.product_revision.trim() == r
|
||||||
// Fallback: match without date
|
&& p.identity.vendor_specific.trim() == vs
|
||||||
.or_else(|| profiles.iter().find(|p| {
|
&& p.identity.firmware_date.trim() == date
|
||||||
p.vendor_id.trim() == v
|
}) {
|
||||||
&& p.product_revision.trim() == r
|
return Some(ProfileMatch { profile: p.clone(), chipset });
|
||||||
&& p.vendor_specific.trim() == vs
|
}
|
||||||
}))
|
|
||||||
|
if let Some(p) = list.iter().find(|p| {
|
||||||
|
p.identity.vendor_id.trim() == v
|
||||||
|
&& p.identity.product_revision.trim() == r
|
||||||
|
&& p.identity.vendor_specific.trim() == vs
|
||||||
|
}) {
|
||||||
|
return Some(ProfileMatch { profile: p.clone(), chipset });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -268,9 +169,9 @@ mod tests {
|
|||||||
fn test_find_known_drive() {
|
fn test_find_known_drive() {
|
||||||
let profiles = load_bundled().unwrap();
|
let profiles = load_bundled().unwrap();
|
||||||
let id = make_drive_id("HL-DT-ST", "1.03", "NM00000", "211810241934");
|
let id = make_drive_id("HL-DT-ST", "1.03", "NM00000", "211810241934");
|
||||||
let p = find_by_drive_id(&profiles, &id).unwrap();
|
let m = find_by_drive_id(&profiles, &id).unwrap();
|
||||||
assert_eq!(p.vendor_id.trim(), "HL-DT-ST");
|
assert_eq!(m.profile.identity.vendor_id.trim(), "HL-DT-ST");
|
||||||
assert_eq!(p.vendor_specific.trim(), "NM00000");
|
assert_eq!(m.chipset, Chipset::MediaTek);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user