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:
MattJackson
2026-04-07 12:12:24 -07:00
parent 721308cb8e
commit 272215c551
14 changed files with 1481 additions and 182 deletions
+17 -16
View File
@@ -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***
+6 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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};
-4
View File
@@ -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
View File
@@ -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
View File
@@ -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 {