diff --git a/src/disc/mod.rs b/src/disc/mod.rs index 61c2393..c039b12 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -2269,6 +2269,22 @@ impl Disc { .unwrap_or_default() } + /// The unlocker matrix for this scanned disc on `drive`: each REGISTERED + /// unlocker's name + whether it applies to this drive + disc. Registry-driven + /// (no hardcoded names) so the CLI and autorip render an identical, always- + /// current report. The disc's crypto kind is derived here (in the library) so + /// both apps agree. + pub fn unlocker_matrix(&self, drive: &crate::Drive) -> Vec<(&'static str, bool)> { + let kind = if self.aacs.is_some() || self.aacs_error.is_some() { + freemkv_unlock::DiscKind::Aacs + } else if self.css.is_some() || self.css_error.is_some() { + freemkv_unlock::DiscKind::Css + } else { + freemkv_unlock::DiscKind::Unencrypted + }; + crate::unlock_bridge::unlocker_matrix(&drive.drive_id, kind) + } + /// The system-wide decrypt correctness gate. /// /// Returns `Ok(())` when it is safe to proceed with a copy or mux, and a diff --git a/src/drive/mod.rs b/src/drive/mod.rs index f9342c6..3535146 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -226,6 +226,15 @@ impl Drive { crate::unlock_bridge::unlocker_name(&self.drive_id).is_some() } + /// The name of the drive unlocker that ACTUALLY unlocked this drive at + /// `init()`, or `None` if none applied (unsupported drive, or the unlock + /// failed). Distinct from [`has_profile`](Self::has_profile), which reports + /// only an identity match: this is the runtime outcome. Apps render it in the + /// user-facing unlocker report. + pub fn unlocker_name(&self) -> Option<&str> { + self.unlocker_name.as_deref() + } + /// Access the SCSI transport for direct commands (used by CSS/AACS auth). pub fn scsi_mut(&mut self) -> &mut dyn ScsiTransport { self.scsi.as_mut() diff --git a/src/unlock_bridge.rs b/src/unlock_bridge.rs index 87fa4d6..c9496e4 100644 --- a/src/unlock_bridge.rs +++ b/src/unlock_bridge.rs @@ -124,6 +124,23 @@ pub(crate) fn run_unlockers( Err(fu::UnlockError::NotApplicable) } +/// Registry-driven unlocker matrix: each REGISTERED unlocker's name + whether it +/// applies to this drive + disc, in dispatch order. Dynamic — sourced from +/// `all_unlockers()`, so adding/removing an unlocker updates every report with no +/// other change (no hardcoded names). Apps render the yes/no English from this +/// typed list. Report-only: uses `applies_to`, not the phase-gated `matches`. +pub(crate) fn unlocker_matrix( + drive_id: &crate::identity::DriveId, + kind: fu::DiscKind, +) -> Vec<(&'static str, bool)> { + let id = to_fu_drive_id(drive_id); + let ctx = fu::UnlockCtx::new(&id, kind, &[]); + fu::all_unlockers() + .iter() + .map(|u| (u.name(), u.applies_to(&ctx))) + .collect() +} + #[cfg(test)] mod tests { use super::*;