v0.7.1: SectorReader trait, IsoStream, StdioStream, resolve_encryption
- SectorReader trait decouples disc scanning from SCSI - Disc::scan_image() for ISO and any sector source - resolve_encryption() handles AACS 1.0/2.0/none in one path - IsoStream: full UDF/MPLS/CLPI/labels pipeline from ISO files - StdioStream: stdin/stdout pipe - Strict scheme:// URL format with validation - Labels module refactored to SectorReader - 7 stream types total
This commit is contained in:
@@ -1,5 +1,39 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.7.1 (2026-04-11)
|
||||||
|
|
||||||
|
### SectorReader trait
|
||||||
|
|
||||||
|
- **`SectorReader` trait** — decouples disc scanning from SCSI. UDF, MPLS, CLPI, labels, and AACS resolution now work with any sector source.
|
||||||
|
- **`Disc::scan_image()`** — scan ISO images or any SectorReader. Full title/stream/label/AACS pipeline, no drive required.
|
||||||
|
- **`resolve_encryption()`** — single function handles AACS 1.0, 2.0, or none. Uses whatever path works (KEYDB VUK, handshake, media key, device key).
|
||||||
|
|
||||||
|
### Stream types
|
||||||
|
|
||||||
|
- **7 stream types** — Disc, ISO, MKV, M2TS, Network, Stdio, Null
|
||||||
|
- **`IsoStream`** — read/write Blu-ray ISO images. Uses `Disc::scan_image()` for full UDF parsing (not heuristic scanning).
|
||||||
|
- **`StdioStream`** — stdin/stdout pipe, format-agnostic
|
||||||
|
- **Strict URL format** — all URLs require `scheme://path`. Bare paths rejected with clear error messages.
|
||||||
|
- **Validation** — empty paths, missing ports, read-only/write-only direction errors
|
||||||
|
|
||||||
|
### IOStream trait
|
||||||
|
|
||||||
|
- `IOStream` trait for all stream types (Read + Write + info + finish)
|
||||||
|
- `open_input()` / `open_output()` resolve URL strings to stream instances
|
||||||
|
|
||||||
|
## 0.7.0 (2026-04-11)
|
||||||
|
|
||||||
|
### Stream I/O architecture
|
||||||
|
|
||||||
|
- **5 stream types** — Disc, MKV, M2TS, Network, Null
|
||||||
|
- **`IOStream` trait** — common interface for all streams
|
||||||
|
- **URL resolver** — `open_input()` / `open_output()` with scheme://path format
|
||||||
|
- **FMKV metadata header** — JSON metadata embedded in M2TS and network streams
|
||||||
|
- **Bidirectional MKV** — MkvStream reads and writes Matroska containers
|
||||||
|
- **Network streaming** — TCP with metadata header, TCP_NODELAY
|
||||||
|
- **BD-TS demuxer** — PAT/PMT scanning, PTS duration detection
|
||||||
|
- **EBML reader** — parse existing MKV files for read-side MkvStream
|
||||||
|
|
||||||
## 0.6.0 (2026-04-10)
|
## 0.6.0 (2026-04-10)
|
||||||
|
|
||||||
### API improvements
|
### API improvements
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "libfreemkv"
|
name = "libfreemkv"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
description = "Open source raw disc access library for optical drives"
|
description = "Open source raw disc access library for optical drives"
|
||||||
|
|||||||
+97
-59
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::drive::DriveSession;
|
use crate::drive::DriveSession;
|
||||||
|
use crate::sector::SectorReader;
|
||||||
use crate::speed::DriveSpeed;
|
use crate::speed::DriveSpeed;
|
||||||
use crate::udf;
|
use crate::udf;
|
||||||
use crate::mpls;
|
use crate::mpls;
|
||||||
@@ -312,7 +313,16 @@ impl DiscTitle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ─── AACS state ─────────────────────────────────────────────────────────────
|
// ─── Encryption ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Result of SCSI AACS handshake (ECDH authentication).
|
||||||
|
/// Only available when scanning from a real drive, not ISO images.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct HandshakeResult {
|
||||||
|
volume_id: [u8; 16],
|
||||||
|
read_data_key: Option<[u8; 16]>,
|
||||||
|
error: Option<crate::error::Error>,
|
||||||
|
}
|
||||||
|
|
||||||
/// AACS decryption state for a disc.
|
/// AACS decryption state for a disc.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -495,19 +505,34 @@ impl Disc {
|
|||||||
/// The session must be open and unlocked (DriveSession::open handles this).
|
/// The session must be open and unlocked (DriveSession::open handles this).
|
||||||
/// All disc reads use standard READ(10) via UDF -- no vendor SCSI commands.
|
/// All disc reads use standard READ(10) via UDF -- no vendor SCSI commands.
|
||||||
pub fn scan(session: &mut DriveSession, opts: &ScanOptions) -> Result<Self> {
|
pub fn scan(session: &mut DriveSession, opts: &ScanOptions) -> Result<Self> {
|
||||||
// 1. Capacity
|
|
||||||
let capacity = Self::read_capacity(session)?;
|
let capacity = Self::read_capacity(session)?;
|
||||||
|
let handshake = Self::do_handshake(session, opts);
|
||||||
|
Self::scan_with(session, capacity, handshake, opts)
|
||||||
|
}
|
||||||
|
|
||||||
// 2. UDF filesystem
|
/// Scan a disc image (ISO or any SectorReader). No SCSI, no handshake.
|
||||||
let udf_fs = udf::read_filesystem(session)?;
|
/// AACS resolution uses KEYDB VUK lookup only.
|
||||||
|
pub fn scan_image(reader: &mut dyn SectorReader, capacity: u32, opts: &ScanOptions) -> Result<Self> {
|
||||||
|
Self::scan_with(reader, capacity, None, opts)
|
||||||
|
}
|
||||||
|
|
||||||
// 3. AACS -- read files from disc via UDF, resolve keys via KEYDB
|
/// Core scan pipeline — works with any SectorReader.
|
||||||
|
fn scan_with(
|
||||||
|
reader: &mut dyn SectorReader,
|
||||||
|
capacity: u32,
|
||||||
|
handshake: Option<HandshakeResult>,
|
||||||
|
opts: &ScanOptions,
|
||||||
|
) -> Result<Self> {
|
||||||
|
// 1. UDF filesystem
|
||||||
|
let udf_fs = udf::read_filesystem(reader)?;
|
||||||
|
|
||||||
|
// 2. Resolve encryption (AACS, CSS, or none)
|
||||||
let encrypted = udf_fs.find_dir("/AACS").is_some()
|
let encrypted = udf_fs.find_dir("/AACS").is_some()
|
||||||
|| udf_fs.find_dir("/BDMV/AACS").is_some();
|
|| udf_fs.find_dir("/BDMV/AACS").is_some();
|
||||||
|
|
||||||
let aacs = if encrypted {
|
let aacs = if encrypted {
|
||||||
if let Some(keydb_path) = opts.resolve_keydb() {
|
if let Some(keydb_path) = opts.resolve_keydb() {
|
||||||
Self::resolve_aacs(&udf_fs, session, &keydb_path).ok()
|
Self::resolve_encryption(&udf_fs, reader, &keydb_path, handshake.as_ref()).ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -515,14 +540,14 @@ impl Disc {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Playlists
|
// 3. Playlists
|
||||||
let mut titles = Vec::new();
|
let mut titles = Vec::new();
|
||||||
if let Some(playlist_dir) = udf_fs.find_dir("/BDMV/PLAYLIST") {
|
if let Some(playlist_dir) = udf_fs.find_dir("/BDMV/PLAYLIST") {
|
||||||
for entry in &playlist_dir.entries {
|
for entry in &playlist_dir.entries {
|
||||||
if !entry.is_dir && entry.name.to_lowercase().ends_with(".mpls") {
|
if !entry.is_dir && entry.name.to_lowercase().ends_with(".mpls") {
|
||||||
let path = format!("/BDMV/PLAYLIST/{}", entry.name);
|
let path = format!("/BDMV/PLAYLIST/{}", entry.name);
|
||||||
if let Ok(mpls_data) = udf_fs.read_file(session, &path) {
|
if let Ok(mpls_data) = udf_fs.read_file(reader, &path) {
|
||||||
if let Some(title) = Self::parse_playlist(session, &udf_fs, &entry.name, &mpls_data) {
|
if let Some(title) = Self::parse_playlist(reader, &udf_fs, &entry.name, &mpls_data) {
|
||||||
titles.push(title);
|
titles.push(title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,11 +556,11 @@ impl Disc {
|
|||||||
}
|
}
|
||||||
titles.sort_by(|a, b| b.duration_secs.partial_cmp(&a.duration_secs).unwrap_or(std::cmp::Ordering::Equal));
|
titles.sort_by(|a, b| b.duration_secs.partial_cmp(&a.duration_secs).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
|
||||||
// 5. Metadata + labels
|
// 4. Metadata + labels
|
||||||
let meta_title = Self::read_meta_title(session, &udf_fs);
|
let meta_title = Self::read_meta_title(reader, &udf_fs);
|
||||||
crate::labels::apply(session, &udf_fs, &mut titles);
|
crate::labels::apply(reader, &udf_fs, &mut titles);
|
||||||
|
|
||||||
// 6. Derive format, layers, region
|
// 5. Derive format, layers, region
|
||||||
let format = Self::detect_format(&titles);
|
let format = Self::detect_format(&titles);
|
||||||
let layers = if capacity > 24_000_000 { 2 } else { 1 };
|
let layers = if capacity > 24_000_000 { 2 } else { 1 };
|
||||||
let region = if format == DiscFormat::Uhd { DiscRegion::Free } else { DiscRegion::Free };
|
let region = if format == DiscFormat::Uhd { DiscRegion::Free } else { DiscRegion::Free };
|
||||||
@@ -554,61 +579,74 @@ impl Disc {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve AACS keys from disc files + KEYDB. No SCSI commands.
|
/// SCSI handshake result — volume ID and bus keys from ECDH authentication.
|
||||||
/// Reads Unit_Key_RO.inf, Content Certificate, and MKB from UDF.
|
/// Only available when scanning from a real drive (not ISO images).
|
||||||
fn resolve_aacs(
|
fn do_handshake(session: &mut DriveSession, opts: &ScanOptions) -> Option<HandshakeResult> {
|
||||||
udf_fs: &udf::UdfFs,
|
|
||||||
session: &mut DriveSession,
|
|
||||||
keydb_path: &std::path::Path,
|
|
||||||
) -> Result<AacsState> {
|
|
||||||
use crate::aacs::{self, KeyDb};
|
use crate::aacs::{self, KeyDb};
|
||||||
|
|
||||||
let keydb = KeyDb::load(keydb_path).map_err(|_| Error::KeydbLoad { path: keydb_path.display().to_string() })?;
|
let keydb_path = opts.resolve_keydb()?;
|
||||||
|
let keydb = KeyDb::load(&keydb_path).ok()?;
|
||||||
// Read AACS files from disc via UDF (standard READ(10), no vendor commands)
|
|
||||||
let uk_ro_data = udf_fs.read_file(session, "/AACS/Unit_Key_RO.inf")
|
|
||||||
.or_else(|_| udf_fs.read_file(session, "/AACS/DUPLICATE/Unit_Key_RO.inf"))
|
|
||||||
.map_err(|_| Error::AacsNoKeys)?;
|
|
||||||
|
|
||||||
let cc_data = udf_fs.read_file(session, "/AACS/Content000.cer")
|
|
||||||
.or_else(|_| udf_fs.read_file(session, "/AACS/Content001.cer"))
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let mkb_data = udf_fs.read_file(session, "/AACS/MKB_RW.inf")
|
|
||||||
.or_else(|_| udf_fs.read_file(session, "/AACS/MKB_RO.inf"))
|
|
||||||
.ok();
|
|
||||||
let mkb_ver = mkb_data.as_deref().and_then(aacs::mkb_version);
|
|
||||||
|
|
||||||
// AACS SCSI handshake — get Volume ID (and read data key for AACS 2.0)
|
|
||||||
let mut volume_id = [0u8; 16];
|
|
||||||
let mut read_data_key = None;
|
|
||||||
let mut handshake_error = None;
|
|
||||||
|
|
||||||
for hc in &keydb.host_certs {
|
for hc in &keydb.host_certs {
|
||||||
match aacs::handshake::aacs_authenticate(
|
match aacs::handshake::aacs_authenticate(
|
||||||
session, &hc.private_key, &hc.certificate,
|
session, &hc.private_key, &hc.certificate,
|
||||||
) {
|
) {
|
||||||
Ok(mut auth) => {
|
Ok(mut auth) => {
|
||||||
// Read Volume ID (needed for MK → VUK derivation)
|
let volume_id = aacs::handshake::read_volume_id(session, &mut auth)
|
||||||
if let Ok(vid) = aacs::handshake::read_volume_id(session, &mut auth) {
|
.unwrap_or([0u8; 16]);
|
||||||
volume_id = vid;
|
let read_data_key = aacs::handshake::read_data_keys(session, &mut auth)
|
||||||
}
|
.ok().map(|(rdk, _)| rdk);
|
||||||
|
return Some(HandshakeResult { volume_id, read_data_key, error: None });
|
||||||
// Read data keys for bus decryption (AACS 2.0 / UHD)
|
|
||||||
if let Ok((rdk, _wdk)) = aacs::handshake::read_data_keys(session, &mut auth) {
|
|
||||||
read_data_key = Some(rdk);
|
|
||||||
}
|
|
||||||
|
|
||||||
handshake_error = None;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
handshake_error = Some(e);
|
// Try next host cert
|
||||||
|
return Some(HandshakeResult {
|
||||||
|
volume_id: [0u8; 16],
|
||||||
|
read_data_key: None,
|
||||||
|
error: Some(e),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve: disc hash → KEYDB lookup → VUK → unit keys
|
/// Resolve disc encryption — AACS 1.0, AACS 2.0, CSS, or none.
|
||||||
|
///
|
||||||
|
/// Reads AACS files from UDF (via SectorReader), resolves keys through
|
||||||
|
/// whatever path works: KEYDB VUK lookup, media key derivation, processing
|
||||||
|
/// keys, device keys. Uses handshake result (volume ID, bus key) if available.
|
||||||
|
fn resolve_encryption(
|
||||||
|
udf_fs: &udf::UdfFs,
|
||||||
|
reader: &mut dyn SectorReader,
|
||||||
|
keydb_path: &std::path::Path,
|
||||||
|
handshake: Option<&HandshakeResult>,
|
||||||
|
) -> Result<AacsState> {
|
||||||
|
use crate::aacs::{self, KeyDb};
|
||||||
|
|
||||||
|
let keydb = KeyDb::load(keydb_path).map_err(|_| Error::KeydbLoad { path: keydb_path.display().to_string() })?;
|
||||||
|
|
||||||
|
// Read AACS files from disc/image via UDF
|
||||||
|
let uk_ro_data = udf_fs.read_file(reader, "/AACS/Unit_Key_RO.inf")
|
||||||
|
.or_else(|_| udf_fs.read_file(reader, "/AACS/DUPLICATE/Unit_Key_RO.inf"))
|
||||||
|
.map_err(|_| Error::AacsNoKeys)?;
|
||||||
|
|
||||||
|
let cc_data = udf_fs.read_file(reader, "/AACS/Content000.cer")
|
||||||
|
.or_else(|_| udf_fs.read_file(reader, "/AACS/Content001.cer"))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let mkb_data = udf_fs.read_file(reader, "/AACS/MKB_RW.inf")
|
||||||
|
.or_else(|_| udf_fs.read_file(reader, "/AACS/MKB_RO.inf"))
|
||||||
|
.ok();
|
||||||
|
let mkb_ver = mkb_data.as_deref().and_then(aacs::mkb_version);
|
||||||
|
|
||||||
|
// Use handshake volume ID if available, otherwise zeros
|
||||||
|
// (KEYDB VUK lookup by disc hash works without volume ID)
|
||||||
|
let volume_id = handshake.map(|h| h.volume_id).unwrap_or([0u8; 16]);
|
||||||
|
let read_data_key = handshake.and_then(|h| h.read_data_key);
|
||||||
|
let handshake_error = None;
|
||||||
|
|
||||||
|
// Resolve: tries all available paths — KEYDB VUK, media key, processing key, device key
|
||||||
let resolved = aacs::resolve_keys(
|
let resolved = aacs::resolve_keys(
|
||||||
&uk_ro_data,
|
&uk_ro_data,
|
||||||
cc_data.as_deref(),
|
cc_data.as_deref(),
|
||||||
@@ -662,7 +700,7 @@ impl Disc {
|
|||||||
/// Read disc title from META/DL/bdmt_eng.xml (Blu-ray Disc Meta Table).
|
/// Read disc title from META/DL/bdmt_eng.xml (Blu-ray Disc Meta Table).
|
||||||
/// Prefers English, falls back to first available language.
|
/// Prefers English, falls back to first available language.
|
||||||
/// Returns None if META directory is empty or XML has no usable title.
|
/// Returns None if META directory is empty or XML has no usable title.
|
||||||
fn read_meta_title(session: &mut DriveSession, udf_fs: &udf::UdfFs) -> Option<String> {
|
fn read_meta_title(reader: &mut dyn SectorReader, udf_fs: &udf::UdfFs) -> Option<String> {
|
||||||
let meta_dir = udf_fs.find_dir("/BDMV/META")?;
|
let meta_dir = udf_fs.find_dir("/BDMV/META")?;
|
||||||
for sub in &meta_dir.entries {
|
for sub in &meta_dir.entries {
|
||||||
if !sub.is_dir { continue; }
|
if !sub.is_dir { continue; }
|
||||||
@@ -677,7 +715,7 @@ impl Disc {
|
|||||||
|
|
||||||
if let Some(entry) = target {
|
if let Some(entry) = target {
|
||||||
let path = format!("{}/{}", dl_path, entry.name);
|
let path = format!("{}/{}", dl_path, entry.name);
|
||||||
if let Ok(data) = udf_fs.read_file(session, &path) {
|
if let Ok(data) = udf_fs.read_file(reader, &path) {
|
||||||
let xml = String::from_utf8_lossy(&data);
|
let xml = String::from_utf8_lossy(&data);
|
||||||
if let Some(start) = xml.find("<di:name>") {
|
if let Some(start) = xml.find("<di:name>") {
|
||||||
let s = start + "<di:name>".len();
|
let s = start + "<di:name>".len();
|
||||||
@@ -704,7 +742,7 @@ impl Disc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_playlist(
|
fn parse_playlist(
|
||||||
session: &mut DriveSession,
|
reader: &mut dyn SectorReader,
|
||||||
udf_fs: &udf::UdfFs,
|
udf_fs: &udf::UdfFs,
|
||||||
filename: &str,
|
filename: &str,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
@@ -732,7 +770,7 @@ impl Disc {
|
|||||||
let mut pkt_count: u32 = 0;
|
let mut pkt_count: u32 = 0;
|
||||||
|
|
||||||
let clpi_path = format!("/BDMV/CLIPINF/{}.clpi", play_item.clip_id);
|
let clpi_path = format!("/BDMV/CLIPINF/{}.clpi", play_item.clip_id);
|
||||||
if let Ok(clpi_data) = udf_fs.read_file(session, &clpi_path) {
|
if let Ok(clpi_data) = udf_fs.read_file(reader, &clpi_path) {
|
||||||
if let Ok(clip_info) = clpi::parse(&clpi_data) {
|
if let Ok(clip_info) = clpi::parse(&clpi_data) {
|
||||||
pkt_count = clip_info.source_packet_count;
|
pkt_count = clip_info.source_packet_count;
|
||||||
total_size += pkt_count as u64 * 192;
|
total_size += pkt_count as u64 * 192;
|
||||||
@@ -740,7 +778,7 @@ impl Disc {
|
|||||||
// Get m2ts file start LBA and compute extent from packet count.
|
// Get m2ts file start LBA and compute extent from packet count.
|
||||||
// BD-ROM m2ts files are contiguous on disc (mastering requirement).
|
// BD-ROM m2ts files are contiguous on disc (mastering requirement).
|
||||||
let m2ts_path = format!("/BDMV/STREAM/{}.m2ts", play_item.clip_id);
|
let m2ts_path = format!("/BDMV/STREAM/{}.m2ts", play_item.clip_id);
|
||||||
let file_lba = udf_fs.file_start_lba(session, &m2ts_path).unwrap_or(0);
|
let file_lba = udf_fs.file_start_lba(reader, &m2ts_path).unwrap_or(0);
|
||||||
let total_bytes = pkt_count as u64 * 192;
|
let total_bytes = pkt_count as u64 * 192;
|
||||||
let total_sectors = ((total_bytes + 2047) / 2048) as u32;
|
let total_sectors = ((total_bytes + 2047) / 2048) as u32;
|
||||||
if total_sectors > 0 && file_lba > 0 {
|
if total_sectors > 0 && file_lba > 0 {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
use crate::sector::SectorReader;
|
||||||
use crate::scsi::ScsiTransport;
|
use crate::scsi::ScsiTransport;
|
||||||
use crate::identity::DriveId;
|
use crate::identity::DriveId;
|
||||||
use crate::profile::{self, DriveProfile};
|
use crate::profile::{self, DriveProfile};
|
||||||
@@ -160,6 +161,12 @@ impl DriveSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SectorReader for DriveSession {
|
||||||
|
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||||
|
self.read_disc(lba, count, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! Clean structured XML with Content/Qualifier per stream and
|
//! Clean structured XML with Content/Qualifier per stream and
|
||||||
//! stream number mapping via playbackconfig.
|
//! stream number mapping via playbackconfig.
|
||||||
|
|
||||||
use crate::drive::DriveSession;
|
use crate::sector::SectorReader;
|
||||||
use crate::udf::UdfFs;
|
use crate::udf::UdfFs;
|
||||||
use super::{StreamLabel, StreamLabelType, LabelPurpose, LabelQualifier};
|
use super::{StreamLabel, StreamLabelType, LabelPurpose, LabelQualifier};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -12,8 +12,8 @@ pub fn detect(udf: &UdfFs) -> bool {
|
|||||||
super::jar_file_exists(udf, "streamproperties.xml")
|
super::jar_file_exists(udf, "streamproperties.xml")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(session: &mut DriveSession, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
pub fn parse(reader: &mut dyn SectorReader, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
||||||
let sp_data = super::read_jar_file(session, udf, "streamproperties.xml")?;
|
let sp_data = super::read_jar_file(reader, udf, "streamproperties.xml")?;
|
||||||
let sp_text = std::str::from_utf8(&sp_data).ok()?;
|
let sp_text = std::str::from_utf8(&sp_data).ok()?;
|
||||||
|
|
||||||
let stream_infos = parse_stream_infos(sp_text);
|
let stream_infos = parse_stream_infos(sp_text);
|
||||||
@@ -21,7 +21,7 @@ pub fn parse(session: &mut DriveSession, udf: &UdfFs) -> Option<Vec<StreamLabel>
|
|||||||
|
|
||||||
// Stream number mapping from playbackconfig.xml
|
// Stream number mapping from playbackconfig.xml
|
||||||
let mut stream_map: HashMap<String, u16> = HashMap::new();
|
let mut stream_map: HashMap<String, u16> = HashMap::new();
|
||||||
if let Some(pc_data) = super::read_jar_file(session, udf, "playbackconfig.xml") {
|
if let Some(pc_data) = super::read_jar_file(reader, udf, "playbackconfig.xml") {
|
||||||
if let Ok(pc_text) = std::str::from_utf8(&pc_data) {
|
if let Ok(pc_text) = std::str::from_utf8(&pc_data) {
|
||||||
parse_playback_config(pc_text, &mut stream_map);
|
parse_playback_config(pc_text, &mut stream_map);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -4,7 +4,7 @@
|
|||||||
//! When both exist, language_streams.txt provides structured types while
|
//! When both exist, language_streams.txt provides structured types while
|
||||||
//! menu_base.prop provides stream number → button name mapping.
|
//! menu_base.prop provides stream number → button name mapping.
|
||||||
|
|
||||||
use crate::drive::DriveSession;
|
use crate::sector::SectorReader;
|
||||||
use crate::udf::UdfFs;
|
use crate::udf::UdfFs;
|
||||||
use super::{StreamLabel, StreamLabelType, LabelPurpose, LabelQualifier, vocab};
|
use super::{StreamLabel, StreamLabelType, LabelPurpose, LabelQualifier, vocab};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -14,12 +14,12 @@ pub fn detect(udf: &UdfFs) -> bool {
|
|||||||
|| super::jar_file_exists(udf, "language_streams.txt")
|
|| super::jar_file_exists(udf, "language_streams.txt")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(session: &mut DriveSession, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
pub fn parse(reader: &mut dyn SectorReader, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
||||||
// Try language_streams.txt first (richer structured data)
|
// Try language_streams.txt first (richer structured data)
|
||||||
let ls_labels = parse_language_streams(session, udf);
|
let ls_labels = parse_language_streams(reader, udf);
|
||||||
|
|
||||||
// Try menu_base.prop (stream numbers + key names)
|
// Try menu_base.prop (stream numbers + key names)
|
||||||
let mb_labels = parse_menu_base(session, udf);
|
let mb_labels = parse_menu_base(reader, udf);
|
||||||
|
|
||||||
// If we have both, merge: language_streams for structure, menu_base for names
|
// If we have both, merge: language_streams for structure, menu_base for names
|
||||||
match (ls_labels, mb_labels) {
|
match (ls_labels, mb_labels) {
|
||||||
@@ -48,8 +48,8 @@ fn merge(ls: Vec<StreamLabel>, mb: Vec<StreamLabel>) -> Vec<StreamLabel> {
|
|||||||
|
|
||||||
// ── language_streams.txt parser ────────────────────────────────────────────
|
// ── language_streams.txt parser ────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse_language_streams(session: &mut DriveSession, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
fn parse_language_streams(reader: &mut dyn SectorReader, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
||||||
let data = super::read_jar_file(session, udf, "language_streams.txt")?;
|
let data = super::read_jar_file(reader, udf, "language_streams.txt")?;
|
||||||
let text = std::str::from_utf8(&data).ok()?;
|
let text = std::str::from_utf8(&data).ok()?;
|
||||||
|
|
||||||
let mut labels = Vec::new();
|
let mut labels = Vec::new();
|
||||||
@@ -123,8 +123,8 @@ fn parse_language_streams(session: &mut DriveSession, udf: &UdfFs) -> Option<Vec
|
|||||||
|
|
||||||
// ── menu_base.prop parser ──────────────────────────────────────────────────
|
// ── menu_base.prop parser ──────────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse_menu_base(session: &mut DriveSession, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
fn parse_menu_base(reader: &mut dyn SectorReader, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
||||||
let data = super::read_jar_file(session, udf, "menu_base.prop")?;
|
let data = super::read_jar_file(reader, udf, "menu_base.prop")?;
|
||||||
let text = std::str::from_utf8(&data).ok()?;
|
let text = std::str::from_utf8(&data).ok()?;
|
||||||
|
|
||||||
// Parse key=value, group by prefix
|
// Parse key=value, group by prefix
|
||||||
|
|||||||
+9
-9
@@ -4,7 +4,7 @@
|
|||||||
//! To add a new format:
|
//! To add a new format:
|
||||||
//! 1. Create `src/labels/myformat.rs`
|
//! 1. Create `src/labels/myformat.rs`
|
||||||
//! 2. Implement `pub fn detect(udf: &UdfFs) -> bool`
|
//! 2. Implement `pub fn detect(udf: &UdfFs) -> bool`
|
||||||
//! 3. Implement `pub fn parse(session: &mut DriveSession, udf: &UdfFs) -> Option<Vec<StreamLabel>>`
|
//! 3. Implement `pub fn parse(reader: &mut dyn SectorReader, udf: &UdfFs) -> Option<Vec<StreamLabel>>`
|
||||||
//! 4. Add `mod myformat;` below and one line to `PARSERS` array
|
//! 4. Add `mod myformat;` below and one line to `PARSERS` array
|
||||||
|
|
||||||
mod paramount;
|
mod paramount;
|
||||||
@@ -13,7 +13,7 @@ mod pixelogic;
|
|||||||
mod ctrm;
|
mod ctrm;
|
||||||
pub mod vocab;
|
pub mod vocab;
|
||||||
|
|
||||||
use crate::drive::DriveSession;
|
use crate::sector::SectorReader;
|
||||||
use crate::udf::UdfFs;
|
use crate::udf::UdfFs;
|
||||||
use crate::disc::{DiscTitle, Stream};
|
use crate::disc::{DiscTitle, Stream};
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ pub enum LabelQualifier {
|
|||||||
// Order = priority. First match wins. Highest quality output first.
|
// Order = priority. First match wins. Highest quality output first.
|
||||||
|
|
||||||
type DetectFn = fn(&UdfFs) -> bool;
|
type DetectFn = fn(&UdfFs) -> bool;
|
||||||
type ParseFn = fn(&mut DriveSession, &UdfFs) -> Option<Vec<StreamLabel>>;
|
type ParseFn = fn(&mut dyn SectorReader, &UdfFs) -> Option<Vec<StreamLabel>>;
|
||||||
|
|
||||||
const PARSERS: &[(&str, DetectFn, ParseFn)] = &[
|
const PARSERS: &[(&str, DetectFn, ParseFn)] = &[
|
||||||
("paramount", paramount::detect, paramount::parse),
|
("paramount", paramount::detect, paramount::parse),
|
||||||
@@ -79,9 +79,9 @@ const PARSERS: &[(&str, DetectFn, ParseFn)] = &[
|
|||||||
|
|
||||||
/// Search disc for config files, extract labels, apply to streams.
|
/// Search disc for config files, extract labels, apply to streams.
|
||||||
/// This is 100% optional — if anything fails, streams are untouched.
|
/// This is 100% optional — if anything fails, streams are untouched.
|
||||||
pub fn apply(session: &mut DriveSession, udf: &UdfFs, titles: &mut [DiscTitle]) {
|
pub fn apply(reader: &mut dyn SectorReader, udf: &UdfFs, titles: &mut [DiscTitle]) {
|
||||||
let labels = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
let labels = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
extract(session, udf)
|
extract(reader, udf)
|
||||||
})).unwrap_or_default();
|
})).unwrap_or_default();
|
||||||
if labels.is_empty() { return; }
|
if labels.is_empty() { return; }
|
||||||
|
|
||||||
@@ -133,10 +133,10 @@ pub fn apply(session: &mut DriveSession, udf: &UdfFs, titles: &mut [DiscTitle])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract(session: &mut DriveSession, udf: &UdfFs) -> Vec<StreamLabel> {
|
fn extract(reader: &mut dyn SectorReader, udf: &UdfFs) -> Vec<StreamLabel> {
|
||||||
for (_name, detect, parse) in PARSERS {
|
for (_name, detect, parse) in PARSERS {
|
||||||
if detect(udf) {
|
if detect(udf) {
|
||||||
if let Some(labels) = parse(session, udf) {
|
if let Some(labels) = parse(reader, udf) {
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ pub(crate) fn find_jar_file(udf: &UdfFs, filename: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read a file from any BDMV/JAR subdirectory by filename.
|
/// Read a file from any BDMV/JAR subdirectory by filename.
|
||||||
pub(crate) fn read_jar_file(session: &mut DriveSession, udf: &UdfFs, filename: &str) -> Option<Vec<u8>> {
|
pub(crate) fn read_jar_file(reader: &mut dyn SectorReader, udf: &UdfFs, filename: &str) -> Option<Vec<u8>> {
|
||||||
let path = find_jar_file(udf, filename)?;
|
let path = find_jar_file(udf, filename)?;
|
||||||
udf.read_file(session, &path).ok().filter(|d| !d.is_empty())
|
udf.read_file(reader, &path).ok().filter(|d| !d.is_empty())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
//! sub_com1_idx="23,24,25" />
|
//! sub_com1_idx="23,24,25" />
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::drive::DriveSession;
|
use crate::sector::SectorReader;
|
||||||
use crate::udf::UdfFs;
|
use crate::udf::UdfFs;
|
||||||
use super::{StreamLabel, StreamLabelType, LabelPurpose, LabelQualifier};
|
use super::{StreamLabel, StreamLabelType, LabelPurpose, LabelQualifier};
|
||||||
|
|
||||||
@@ -20,8 +20,8 @@ pub fn detect(udf: &UdfFs) -> bool {
|
|||||||
super::jar_file_exists(udf, "playlists.xml")
|
super::jar_file_exists(udf, "playlists.xml")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(session: &mut DriveSession, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
pub fn parse(reader: &mut dyn SectorReader, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
||||||
let data = super::read_jar_file(session, udf, "playlists.xml")?;
|
let data = super::read_jar_file(reader, udf, "playlists.xml")?;
|
||||||
let text = std::str::from_utf8(&data).ok()?;
|
let text = std::str::from_utf8(&data).ok()?;
|
||||||
|
|
||||||
// Find the feature playlist — longest duration or name="Feature"
|
// Find the feature playlist — longest duration or name="Feature"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Token format: `{lang}_{codec?}_{purpose?}_{region?}_`
|
//! Token format: `{lang}_{codec?}_{purpose?}_{region?}_`
|
||||||
|
|
||||||
use crate::drive::DriveSession;
|
use crate::sector::SectorReader;
|
||||||
use crate::udf::UdfFs;
|
use crate::udf::UdfFs;
|
||||||
use super::{StreamLabel, StreamLabelType, LabelPurpose, LabelQualifier, vocab};
|
use super::{StreamLabel, StreamLabelType, LabelPurpose, LabelQualifier, vocab};
|
||||||
|
|
||||||
@@ -18,8 +18,8 @@ pub fn detect(udf: &UdfFs) -> bool {
|
|||||||
super::jar_file_exists(udf, "bluray_project.bin")
|
super::jar_file_exists(udf, "bluray_project.bin")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(session: &mut DriveSession, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
pub fn parse(reader: &mut dyn SectorReader, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
|
||||||
let data = super::read_jar_file(session, udf, "bluray_project.bin")?;
|
let data = super::read_jar_file(reader, udf, "bluray_project.bin")?;
|
||||||
let strings = extract_strings(&data);
|
let strings = extract_strings(&data);
|
||||||
|
|
||||||
let mut labels = Vec::new();
|
let mut labels = Vec::new();
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
//! | E7xxx | AACS errors |
|
//! | E7xxx | AACS errors |
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod sector;
|
||||||
pub mod scsi;
|
pub mod scsi;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
@@ -90,6 +91,7 @@ pub use drive::{DriveSession, find_drive, find_drives, resolve_device};
|
|||||||
pub use identity::DriveId;
|
pub use identity::DriveId;
|
||||||
pub use profile::DriveProfile;
|
pub use profile::DriveProfile;
|
||||||
// Platform trait is pub(crate) -- callers use DriveSession, not Platform directly
|
// Platform trait is pub(crate) -- callers use DriveSession, not Platform directly
|
||||||
|
pub use sector::SectorReader;
|
||||||
pub use scsi::ScsiTransport;
|
pub use scsi::ScsiTransport;
|
||||||
pub use speed::DriveSpeed;
|
pub use speed::DriveSpeed;
|
||||||
pub use disc::{Disc, DiscFormat, DiscTitle, Clip, Stream, VideoStream, AudioStream, SubtitleStream,
|
pub use disc::{Disc, DiscFormat, DiscTitle, Clip, Stream, VideoStream, AudioStream, SubtitleStream,
|
||||||
|
|||||||
+124
-141
@@ -1,54 +1,117 @@
|
|||||||
//! IsoStream — read BD-TS data from a Blu-ray ISO image file.
|
//! IsoStream — read/write Blu-ray ISO disc images.
|
||||||
//!
|
//!
|
||||||
//! Read-only. Parses the UDF filesystem inside the ISO to find
|
//! Read: parses UDF filesystem inside the ISO using the same pipeline as
|
||||||
//! BDMV/STREAM/*.m2ts files, then streams the BD-TS bytes.
|
//! DiscStream (titles, streams, labels, AACS). An ISO is a flat image of
|
||||||
|
//! 2048-byte sectors — sector N starts at byte offset N * 2048.
|
||||||
//!
|
//!
|
||||||
//! An ISO file is a flat image of 2048-byte sectors — the same
|
//! Write: creates a sector-by-sector disc image from a SectorReader source.
|
||||||
//! layout as on a real disc. Sector N starts at byte offset N * 2048.
|
|
||||||
|
|
||||||
use std::io::{self, Read, Write, Seek, SeekFrom};
|
use std::io::{self, Read, Write, Seek, SeekFrom};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use super::IOStream;
|
use super::IOStream;
|
||||||
use crate::disc::DiscTitle;
|
use crate::disc::{Disc, DiscTitle, ScanOptions};
|
||||||
|
use crate::sector::SectorReader;
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
const SECTOR_SIZE: u64 = 2048;
|
const SECTOR_SIZE: u64 = 2048;
|
||||||
|
|
||||||
/// Blu-ray ISO image stream. Read-only.
|
/// File-backed sector reader for ISO images.
|
||||||
|
pub struct IsoSectorReader {
|
||||||
|
file: File,
|
||||||
|
capacity: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IsoSectorReader {
|
||||||
|
pub fn open(path: &str) -> io::Result<Self> {
|
||||||
|
let file = File::open(Path::new(path))
|
||||||
|
.map_err(|e| io::Error::new(e.kind(), format!("iso://{}: {}", path, e)))?;
|
||||||
|
let size = file.metadata()?.len();
|
||||||
|
let capacity = (size / SECTOR_SIZE) as u32;
|
||||||
|
Ok(Self { file, capacity })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn capacity(&self) -> u32 { self.capacity }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SectorReader for IsoSectorReader {
|
||||||
|
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||||
|
let bytes = count as usize * SECTOR_SIZE as usize;
|
||||||
|
self.file.seek(SeekFrom::Start(lba as u64 * SECTOR_SIZE))
|
||||||
|
.map_err(|e| Error::IoError { source: e })?;
|
||||||
|
self.file.read_exact(&mut buf[..bytes])
|
||||||
|
.map_err(|e| Error::IoError { source: e })?;
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blu-ray ISO image stream.
|
||||||
///
|
///
|
||||||
/// Opens an ISO file, parses UDF to locate BDMV playlists and streams,
|
/// Read: opens ISO, parses UDF (same as DiscStream), streams BD-TS content.
|
||||||
/// then reads the m2ts content sectors in order.
|
/// Write: receives sector data and writes to ISO file.
|
||||||
pub struct IsoStream {
|
pub struct IsoStream {
|
||||||
disc_title: DiscTitle,
|
disc_title: DiscTitle,
|
||||||
file: File,
|
disc: Option<Disc>,
|
||||||
|
reader: Option<IsoSectorReader>,
|
||||||
|
writer: Option<io::BufWriter<File>>,
|
||||||
/// Sector ranges to read: (start_lba, sector_count)
|
/// Sector ranges to read: (start_lba, sector_count)
|
||||||
extents: Vec<(u64, u64)>,
|
extents: Vec<(u32, u32)>,
|
||||||
/// Current extent index
|
|
||||||
extent_idx: usize,
|
extent_idx: usize,
|
||||||
/// Sectors remaining in current extent
|
sectors_remaining: u32,
|
||||||
sectors_remaining: u64,
|
|
||||||
/// Read buffer for one sector
|
|
||||||
sector_buf: [u8; SECTOR_SIZE as usize],
|
sector_buf: [u8; SECTOR_SIZE as usize],
|
||||||
/// Position within current sector buffer
|
|
||||||
buf_pos: usize,
|
buf_pos: usize,
|
||||||
/// Bytes valid in sector buffer
|
|
||||||
buf_len: usize,
|
buf_len: usize,
|
||||||
eof: bool,
|
eof: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IsoStream {
|
impl IsoStream {
|
||||||
/// Open an ISO file and scan its contents.
|
/// Open an ISO file for reading. Parses UDF, scans titles, streams, labels.
|
||||||
///
|
pub fn open(path: &str, title_index: Option<usize>, opts: &ScanOptions) -> io::Result<Self> {
|
||||||
/// Parses UDF filesystem, finds playlists and stream extents.
|
let mut reader = IsoSectorReader::open(path)?;
|
||||||
/// The title_index selects which title to read (0-based, default: longest).
|
let capacity = reader.capacity();
|
||||||
pub fn open(path: &str, title_index: Option<usize>) -> io::Result<Self> {
|
|
||||||
let file = File::open(Path::new(path))
|
|
||||||
.map_err(|e| io::Error::new(e.kind(),
|
|
||||||
format!("iso://{}: {}", path, e)))?;
|
|
||||||
|
|
||||||
let mut stream = IsoStream {
|
let disc = Disc::scan_image(&mut reader, capacity, opts)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||||
|
|
||||||
|
let idx = title_index.unwrap_or(0).min(disc.titles.len().saturating_sub(1));
|
||||||
|
let disc_title = if disc.titles.is_empty() {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::NotFound, "no titles found in ISO image"));
|
||||||
|
} else {
|
||||||
|
disc.titles[idx].clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let extents: Vec<(u32, u32)> = disc_title.extents.iter()
|
||||||
|
.map(|e| (e.start_lba, e.sector_count))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let sectors_remaining = extents.first().map(|e| e.1).unwrap_or(0);
|
||||||
|
|
||||||
|
Ok(IsoStream {
|
||||||
|
disc_title,
|
||||||
|
disc: Some(disc),
|
||||||
|
reader: Some(reader),
|
||||||
|
writer: None,
|
||||||
|
extents,
|
||||||
|
extent_idx: 0,
|
||||||
|
sectors_remaining,
|
||||||
|
sector_buf: [0u8; SECTOR_SIZE as usize],
|
||||||
|
buf_pos: 0,
|
||||||
|
buf_len: 0,
|
||||||
|
eof: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an ISO file for writing. Receives raw sector data.
|
||||||
|
pub fn create(path: &str) -> io::Result<Self> {
|
||||||
|
let file = File::create(Path::new(path))
|
||||||
|
.map_err(|e| io::Error::new(e.kind(), format!("iso://{}: {}", path, e)))?;
|
||||||
|
let writer = io::BufWriter::with_capacity(4 * 1024 * 1024, file);
|
||||||
|
|
||||||
|
Ok(IsoStream {
|
||||||
disc_title: DiscTitle::empty(),
|
disc_title: DiscTitle::empty(),
|
||||||
file,
|
disc: None,
|
||||||
|
reader: None,
|
||||||
|
writer: Some(writer),
|
||||||
extents: Vec::new(),
|
extents: Vec::new(),
|
||||||
extent_idx: 0,
|
extent_idx: 0,
|
||||||
sectors_remaining: 0,
|
sectors_remaining: 0,
|
||||||
@@ -56,128 +119,35 @@ impl IsoStream {
|
|||||||
buf_pos: 0,
|
buf_pos: 0,
|
||||||
buf_len: 0,
|
buf_len: 0,
|
||||||
eof: false,
|
eof: false,
|
||||||
};
|
})
|
||||||
|
|
||||||
stream.scan_iso(title_index)?;
|
|
||||||
Ok(stream)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scan the ISO: parse UDF, build title metadata, extract extent map.
|
/// Set metadata (for write mode).
|
||||||
fn scan_iso(&mut self, title_index: Option<usize>) -> io::Result<()> {
|
pub fn meta(mut self, dt: &DiscTitle) -> Self {
|
||||||
// Read AVDP at sector 256 to verify this is a UDF disc image
|
self.disc_title = dt.clone();
|
||||||
let avdp = self.read_sector(256)?;
|
self
|
||||||
let tag_id = u16::from_le_bytes([avdp[0], avdp[1]]);
|
|
||||||
if tag_id != 2 {
|
|
||||||
return Err(io::Error::new(io::ErrorKind::InvalidData,
|
|
||||||
"not a valid UDF image — no AVDP at sector 256"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, scan BDMV/PLAYLIST and BDMV/STREAM directories
|
|
||||||
// by searching for MPLS and M2TS markers in the UDF metadata.
|
|
||||||
//
|
|
||||||
// Full UDF parsing (AVDP → VDS → metadata → FSD → root → files)
|
|
||||||
// will be refactored out of udf.rs to work with both DriveSession
|
|
||||||
// and file-backed sector reads. For now, find the main m2ts file
|
|
||||||
// by scanning for the stream file extents in the UDF file entries.
|
|
||||||
|
|
||||||
// Find all .m2ts file extents from UDF metadata
|
|
||||||
let disc_size = self.file.seek(SeekFrom::End(0))?;
|
|
||||||
let total_sectors = disc_size / SECTOR_SIZE;
|
|
||||||
self.file.seek(SeekFrom::Start(0))?;
|
|
||||||
|
|
||||||
// Scan UDF partition for BDMV structure
|
|
||||||
let titles = self.find_stream_extents(total_sectors)?;
|
|
||||||
|
|
||||||
if titles.is_empty() {
|
|
||||||
return Err(io::Error::new(io::ErrorKind::NotFound,
|
|
||||||
"no BD stream files found in ISO image"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select title
|
|
||||||
let idx = title_index.unwrap_or(0).min(titles.len() - 1);
|
|
||||||
let (title, extents) = &titles[idx];
|
|
||||||
|
|
||||||
self.disc_title = title.clone();
|
|
||||||
self.extents = extents.clone();
|
|
||||||
|
|
||||||
if !self.extents.is_empty() {
|
|
||||||
self.sectors_remaining = self.extents[0].1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a single sector from the ISO file.
|
/// Get the full Disc (for listing all titles).
|
||||||
fn read_sector(&mut self, lba: u64) -> io::Result<Vec<u8>> {
|
pub fn disc(&self) -> Option<&Disc> { self.disc.as_ref() }
|
||||||
let mut buf = vec![0u8; SECTOR_SIZE as usize];
|
|
||||||
self.file.seek(SeekFrom::Start(lba * SECTOR_SIZE))?;
|
|
||||||
self.file.read_exact(&mut buf)?;
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scan the ISO for BD stream file extents.
|
|
||||||
///
|
|
||||||
/// Returns: Vec of (DiscTitle, Vec<(start_lba, sector_count)>)
|
|
||||||
///
|
|
||||||
/// This is a simplified scanner that finds m2ts content by looking
|
|
||||||
/// for 192-byte BD-TS packet boundaries (0x47 sync byte at offset 4).
|
|
||||||
/// Full UDF parsing will replace this once udf.rs is decoupled from DriveSession.
|
|
||||||
fn find_stream_extents(&mut self, total_sectors: u64) -> io::Result<Vec<(DiscTitle, Vec<(u64, u64)>)>> {
|
|
||||||
// Strategy: scan the UDF file entry area for allocation descriptors
|
|
||||||
// pointing to large contiguous regions (m2ts files are large).
|
|
||||||
//
|
|
||||||
// For a BD-ROM ISO, the main m2ts typically starts after the BDMV
|
|
||||||
// metadata (around sector 1000-5000) and runs contiguously to the end.
|
|
||||||
//
|
|
||||||
// Quick approach: find first sector with BD-TS sync (0x47 at byte 4)
|
|
||||||
// and treat everything from there to the end as one extent.
|
|
||||||
|
|
||||||
let probe_start = 256u64; // skip lead-in
|
|
||||||
let probe_end = total_sectors.min(10000); // probe first 20 MB
|
|
||||||
|
|
||||||
let mut stream_start: Option<u64> = None;
|
|
||||||
|
|
||||||
for lba in probe_start..probe_end {
|
|
||||||
let sector = self.read_sector(lba)?;
|
|
||||||
// BD-TS: 192-byte packets, sync byte 0x47 at offset 4 of each packet
|
|
||||||
// A sector (2048 bytes) holds partial packets, but the sync pattern
|
|
||||||
// should appear at regular intervals
|
|
||||||
if sector.len() >= 196 && sector[4] == 0x47 {
|
|
||||||
// Verify: check for another sync at offset 196 (4 + 192)
|
|
||||||
if sector[196] == 0x47 {
|
|
||||||
stream_start = Some(lba);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match stream_start {
|
|
||||||
Some(start) => {
|
|
||||||
let sector_count = total_sectors - start;
|
|
||||||
let size_bytes = sector_count * SECTOR_SIZE;
|
|
||||||
|
|
||||||
let mut title = DiscTitle::empty();
|
|
||||||
title.playlist = "Main Title".into();
|
|
||||||
title.size_bytes = size_bytes;
|
|
||||||
|
|
||||||
Ok(vec![(title, vec![(start, sector_count)])])
|
|
||||||
}
|
|
||||||
None => Ok(Vec::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read the next sector from the current extent.
|
/// Read the next sector from the current extent.
|
||||||
fn read_next_sector(&mut self) -> io::Result<bool> {
|
fn read_next_sector(&mut self) -> io::Result<bool> {
|
||||||
|
let reader = match self.reader.as_mut() {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return Ok(false),
|
||||||
|
};
|
||||||
|
|
||||||
if self.extent_idx >= self.extents.len() {
|
if self.extent_idx >= self.extents.len() {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (start_lba, _) = self.extents[self.extent_idx];
|
let (start_lba, total) = self.extents[self.extent_idx];
|
||||||
let offset = self.extents[self.extent_idx].1 - self.sectors_remaining;
|
let offset = total - self.sectors_remaining;
|
||||||
let lba = start_lba + offset;
|
let lba = start_lba + offset;
|
||||||
|
|
||||||
self.file.seek(SeekFrom::Start(lba * SECTOR_SIZE))?;
|
reader.read_sectors(lba, 1, &mut self.sector_buf)
|
||||||
self.file.read_exact(&mut self.sector_buf)?;
|
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||||
self.buf_pos = 0;
|
self.buf_pos = 0;
|
||||||
self.buf_len = SECTOR_SIZE as usize;
|
self.buf_len = SECTOR_SIZE as usize;
|
||||||
|
|
||||||
@@ -195,7 +165,12 @@ impl IsoStream {
|
|||||||
|
|
||||||
impl IOStream for IsoStream {
|
impl IOStream for IsoStream {
|
||||||
fn info(&self) -> &DiscTitle { &self.disc_title }
|
fn info(&self) -> &DiscTitle { &self.disc_title }
|
||||||
fn finish(&mut self) -> io::Result<()> { Ok(()) }
|
fn finish(&mut self) -> io::Result<()> {
|
||||||
|
if let Some(ref mut w) = self.writer {
|
||||||
|
w.flush()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Read for IsoStream {
|
impl Read for IsoStream {
|
||||||
@@ -224,9 +199,17 @@ impl Read for IsoStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Write for IsoStream {
|
impl Write for IsoStream {
|
||||||
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
Err(io::Error::new(io::ErrorKind::Unsupported,
|
match self.writer.as_mut() {
|
||||||
"iso:// is read-only"))
|
Some(w) => w.write(buf),
|
||||||
|
None => Err(io::Error::new(io::ErrorKind::Unsupported,
|
||||||
|
"iso:// opened for reading — cannot write")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
match self.writer.as_mut() {
|
||||||
|
Some(w) => w.flush(),
|
||||||
|
None => Ok(()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn flush(&mut self) -> io::Result<()> { Ok(()) }
|
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-3
@@ -141,7 +141,11 @@ pub fn open_input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn IOStream
|
|||||||
}
|
}
|
||||||
"iso" => {
|
"iso" => {
|
||||||
validate_file_path(&parsed.path, "iso")?;
|
validate_file_path(&parsed.path, "iso")?;
|
||||||
Ok(Box::new(IsoStream::open(&parsed.path, opts.title_index)?))
|
let scan_opts = match &opts.keydb_path {
|
||||||
|
Some(p) => crate::disc::ScanOptions::with_keydb(p),
|
||||||
|
None => crate::disc::ScanOptions::default(),
|
||||||
|
};
|
||||||
|
Ok(Box::new(IsoStream::open(&parsed.path, opts.title_index, &scan_opts)?))
|
||||||
}
|
}
|
||||||
"null" => {
|
"null" => {
|
||||||
Err(io::Error::new(io::ErrorKind::InvalidInput,
|
Err(io::Error::new(io::ErrorKind::InvalidInput,
|
||||||
@@ -166,8 +170,8 @@ pub fn open_output(url: &str, meta: &DiscTitle) -> io::Result<Box<dyn IOStream>>
|
|||||||
"disc:// is read-only — cannot use as output"))
|
"disc:// is read-only — cannot use as output"))
|
||||||
}
|
}
|
||||||
"iso" => {
|
"iso" => {
|
||||||
Err(io::Error::new(io::ErrorKind::Unsupported,
|
validate_file_path(&parsed.path, "iso")?;
|
||||||
"iso:// is read-only — cannot use as output"))
|
Ok(Box::new(IsoStream::create(&parsed.path)?.meta(meta)))
|
||||||
}
|
}
|
||||||
"null" => {
|
"null" => {
|
||||||
Ok(Box::new(NullStream::new().meta(meta)))
|
Ok(Box::new(NullStream::new().meta(meta)))
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//! SectorReader — trait for reading 2048-byte disc sectors.
|
||||||
|
//!
|
||||||
|
//! Implemented by DriveSession (SCSI) and IsoFile (file-backed).
|
||||||
|
//! Used by UDF parser, disc scanner, label parsers — anything that
|
||||||
|
//! reads sectors doesn't need to know where they come from.
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
/// Read 2048-byte sectors from a disc or disc image.
|
||||||
|
pub trait SectorReader {
|
||||||
|
/// Read `count` sectors starting at `lba` into `buf`.
|
||||||
|
/// `buf` must be at least `count * 2048` bytes.
|
||||||
|
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize>;
|
||||||
|
}
|
||||||
+33
-33
@@ -19,7 +19,7 @@
|
|||||||
//! BD-ROM Part 3 — Blu-ray filesystem profile
|
//! BD-ROM Part 3 — Blu-ray filesystem profile
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::drive::DriveSession;
|
use crate::sector::SectorReader;
|
||||||
|
|
||||||
/// A UDF filesystem parsed from disc.
|
/// A UDF filesystem parsed from disc.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -74,7 +74,7 @@ impl UdfFs {
|
|||||||
/// Reads sector by sector from disc — no buffering.
|
/// Reads sector by sector from disc — no buffering.
|
||||||
/// Get the absolute starting LBA of a file on disc.
|
/// Get the absolute starting LBA of a file on disc.
|
||||||
/// Used by the rip pipeline to locate m2ts content sectors.
|
/// Used by the rip pipeline to locate m2ts content sectors.
|
||||||
pub fn file_start_lba(&self, session: &mut DriveSession, path: &str) -> Result<u32> {
|
pub fn file_start_lba(&self, reader: &mut dyn SectorReader, path: &str) -> Result<u32> {
|
||||||
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
|
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
|
||||||
let mut current = &self.root;
|
let mut current = &self.root;
|
||||||
for part in &parts[..parts.len() - 1] {
|
for part in &parts[..parts.len() - 1] {
|
||||||
@@ -91,11 +91,11 @@ impl UdfFs {
|
|||||||
!e.is_dir && e.name.eq_ignore_ascii_case(filename)
|
!e.is_dir && e.name.eq_ignore_ascii_case(filename)
|
||||||
}).ok_or_else(|| Error::UdfNotFound { path: path.to_string() }
|
}).ok_or_else(|| Error::UdfNotFound { path: path.to_string() }
|
||||||
)?;
|
)?;
|
||||||
let (data_lba, _) = self.read_icb_extent(session, entry.meta_lba)?;
|
let (data_lba, _) = self.read_icb_extent(reader, entry.meta_lba)?;
|
||||||
Ok(self.partition_start + data_lba)
|
Ok(self.partition_start + data_lba)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_file(&self, session: &mut DriveSession, path: &str) -> Result<Vec<u8>> {
|
pub fn read_file(&self, reader: &mut dyn SectorReader, path: &str) -> Result<Vec<u8>> {
|
||||||
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
|
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
|
||||||
let mut current = &self.root;
|
let mut current = &self.root;
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ impl UdfFs {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Read the file's ICB to get its data extent
|
// Read the file's ICB to get its data extent
|
||||||
let (data_lba, data_len) = self.read_icb_extent(session, entry.meta_lba)?;
|
let (data_lba, data_len) = self.read_icb_extent(reader, entry.meta_lba)?;
|
||||||
|
|
||||||
// Read file data sector by sector
|
// Read file data sector by sector
|
||||||
// File DATA is in the physical partition (partition_start + lba),
|
// File DATA is in the physical partition (partition_start + lba),
|
||||||
@@ -129,7 +129,7 @@ impl UdfFs {
|
|||||||
|
|
||||||
for i in 0..sector_count {
|
for i in 0..sector_count {
|
||||||
let offset = (i as usize) * 2048;
|
let offset = (i as usize) * 2048;
|
||||||
read_sector(session, abs_start + i, &mut data[offset..offset + 2048])?;
|
read_sector(reader, abs_start + i, &mut data[offset..offset + 2048])?;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.truncate(entry.size as usize);
|
data.truncate(entry.size as usize);
|
||||||
@@ -145,7 +145,7 @@ impl UdfFs {
|
|||||||
///
|
///
|
||||||
/// Skips: STREAM/ (video), BACKUP/, DUPLICATE/,
|
/// Skips: STREAM/ (video), BACKUP/, DUPLICATE/,
|
||||||
/// MKB_RO.inf, ContentHash*, ContentRevocation*
|
/// MKB_RO.inf, ContentHash*, ContentRevocation*
|
||||||
pub fn metadata_sector_ranges(&self, session: &mut DriveSession) -> Result<Vec<(u32, u32)>> {
|
pub fn metadata_sector_ranges(&self, reader: &mut dyn SectorReader) -> Result<Vec<(u32, u32)>> {
|
||||||
let mut ranges = Vec::new();
|
let mut ranges = Vec::new();
|
||||||
|
|
||||||
// UDF structure: sector 0 through end of metadata partition
|
// UDF structure: sector 0 through end of metadata partition
|
||||||
@@ -154,7 +154,7 @@ impl UdfFs {
|
|||||||
ranges.push((0, meta_end));
|
ranges.push((0, meta_end));
|
||||||
|
|
||||||
// Walk tree, collect ranges for each metadata file
|
// Walk tree, collect ranges for each metadata file
|
||||||
self.collect_file_ranges(session, &self.root, &mut ranges)?;
|
self.collect_file_ranges(reader, &self.root, &mut ranges)?;
|
||||||
|
|
||||||
// Merge overlapping/adjacent ranges and sort
|
// Merge overlapping/adjacent ranges and sort
|
||||||
ranges.sort_by_key(|r| r.0);
|
ranges.sort_by_key(|r| r.0);
|
||||||
@@ -162,14 +162,14 @@ impl UdfFs {
|
|||||||
Ok(merged)
|
Ok(merged)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_file_ranges(&self, session: &mut DriveSession, entry: &DirEntry, ranges: &mut Vec<(u32, u32)>) -> Result<()> {
|
fn collect_file_ranges(&self, reader: &mut dyn SectorReader, entry: &DirEntry, ranges: &mut Vec<(u32, u32)>) -> Result<()> {
|
||||||
for child in &entry.entries {
|
for child in &entry.entries {
|
||||||
if child.is_dir {
|
if child.is_dir {
|
||||||
// Only skip STREAM — those are the multi-GB video files
|
// Only skip STREAM — those are the multi-GB video files
|
||||||
if child.name.eq_ignore_ascii_case("STREAM") {
|
if child.name.eq_ignore_ascii_case("STREAM") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
self.collect_file_ranges(session, child, ranges)?;
|
self.collect_file_ranges(reader, child, ranges)?;
|
||||||
} else {
|
} else {
|
||||||
// Include the ICB sector itself (in metadata partition)
|
// Include the ICB sector itself (in metadata partition)
|
||||||
ranges.push((self.meta_to_abs(child.meta_lba), 1));
|
ranges.push((self.meta_to_abs(child.meta_lba), 1));
|
||||||
@@ -179,7 +179,7 @@ impl UdfFs {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok((data_lba, data_len)) = self.read_icb_extent(session, child.meta_lba) {
|
if let Ok((data_lba, data_len)) = self.read_icb_extent(reader, child.meta_lba) {
|
||||||
let abs_start = self.partition_start + data_lba;
|
let abs_start = self.partition_start + data_lba;
|
||||||
let sector_count = (data_len + 2047) / 2048;
|
let sector_count = (data_len + 2047) / 2048;
|
||||||
ranges.push((abs_start, sector_count));
|
ranges.push((abs_start, sector_count));
|
||||||
@@ -197,17 +197,17 @@ impl UdfFs {
|
|||||||
/// Read an Extended File Entry (tag 266) or File Entry (tag 261)
|
/// Read an Extended File Entry (tag 266) or File Entry (tag 261)
|
||||||
/// and return its first allocation extent: (data_lba, data_length).
|
/// and return its first allocation extent: (data_lba, data_length).
|
||||||
/// The data_lba is partition-relative.
|
/// The data_lba is partition-relative.
|
||||||
fn read_icb_extent(&self, session: &mut DriveSession, meta_lba: u32) -> Result<(u32, u32)> {
|
fn read_icb_extent(&self, reader: &mut dyn SectorReader, meta_lba: u32) -> Result<(u32, u32)> {
|
||||||
let extents = self.read_icb_extents(session, meta_lba)?;
|
let extents = self.read_icb_extents(reader, meta_lba)?;
|
||||||
extents.first().copied().ok_or_else(|| Error::DiscRead { sector: 0 })
|
extents.first().copied().ok_or_else(|| Error::DiscRead { sector: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read ALL allocation extents for a file from its ICB.
|
/// Read ALL allocation extents for a file from its ICB.
|
||||||
/// Returns Vec of (partition_relative_lba, byte_length) pairs.
|
/// Returns Vec of (partition_relative_lba, byte_length) pairs.
|
||||||
/// Handles files with many extents (e.g. 88 GB m2ts files have ~90 extents).
|
/// Handles files with many extents (e.g. 88 GB m2ts files have ~90 extents).
|
||||||
fn read_icb_extents(&self, session: &mut DriveSession, meta_lba: u32) -> Result<Vec<(u32, u32)>> {
|
fn read_icb_extents(&self, reader: &mut dyn SectorReader, meta_lba: u32) -> Result<Vec<(u32, u32)>> {
|
||||||
let mut icb = [0u8; 2048];
|
let mut icb = [0u8; 2048];
|
||||||
read_sector(session, self.meta_to_abs(meta_lba), &mut icb)?;
|
read_sector(reader, self.meta_to_abs(meta_lba), &mut icb)?;
|
||||||
|
|
||||||
let tag = u16::from_le_bytes([icb[0], icb[1]]);
|
let tag = u16::from_le_bytes([icb[0], icb[1]]);
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ impl UdfFs {
|
|||||||
|
|
||||||
/// Get all absolute disc sector extents for a file.
|
/// Get all absolute disc sector extents for a file.
|
||||||
/// Returns Vec of (absolute_lba, sector_count) covering the entire file.
|
/// Returns Vec of (absolute_lba, sector_count) covering the entire file.
|
||||||
pub fn file_extents(&self, session: &mut DriveSession, path: &str) -> Result<Vec<(u32, u32)>> {
|
pub fn file_extents(&self, reader: &mut dyn SectorReader, path: &str) -> Result<Vec<(u32, u32)>> {
|
||||||
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
|
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
|
||||||
let mut current = &self.root;
|
let mut current = &self.root;
|
||||||
for part in &parts[..parts.len() - 1] {
|
for part in &parts[..parts.len() - 1] {
|
||||||
@@ -273,7 +273,7 @@ impl UdfFs {
|
|||||||
}).ok_or_else(|| Error::UdfNotFound { path: path.to_string() }
|
}).ok_or_else(|| Error::UdfNotFound { path: path.to_string() }
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let alloc_extents = self.read_icb_extents(session, entry.meta_lba)?;
|
let alloc_extents = self.read_icb_extents(reader, entry.meta_lba)?;
|
||||||
let mut disc_extents = Vec::new();
|
let mut disc_extents = Vec::new();
|
||||||
for (lba, byte_len) in alloc_extents {
|
for (lba, byte_len) in alloc_extents {
|
||||||
let abs_lba = self.partition_start + lba;
|
let abs_lba = self.partition_start + lba;
|
||||||
@@ -293,11 +293,11 @@ impl UdfFs {
|
|||||||
/// 3. Metadata partition file → metadata content location
|
/// 3. Metadata partition file → metadata content location
|
||||||
/// 4. FSD → root directory ICB
|
/// 4. FSD → root directory ICB
|
||||||
/// 5. Root directory → file tree
|
/// 5. Root directory → file tree
|
||||||
pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
|
pub fn read_filesystem(reader: &mut dyn SectorReader) -> Result<UdfFs> {
|
||||||
// Step 1: Anchor Volume Descriptor Pointer at sector 256
|
// Step 1: Anchor Volume Descriptor Pointer at sector 256
|
||||||
// ECMA-167 §10.2 — always at sector 256
|
// ECMA-167 §10.2 — always at sector 256
|
||||||
let mut avdp = [0u8; 2048];
|
let mut avdp = [0u8; 2048];
|
||||||
read_sector(session, 256, &mut avdp)?;
|
read_sector(reader, 256, &mut avdp)?;
|
||||||
|
|
||||||
let tag_id = u16::from_le_bytes([avdp[0], avdp[1]]);
|
let tag_id = u16::from_le_bytes([avdp[0], avdp[1]]);
|
||||||
if tag_id != 2 {
|
if tag_id != 2 {
|
||||||
@@ -317,7 +317,7 @@ pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
|
|||||||
|
|
||||||
for i in 32..64 {
|
for i in 32..64 {
|
||||||
let mut desc = [0u8; 2048];
|
let mut desc = [0u8; 2048];
|
||||||
read_sector(session, i, &mut desc)?;
|
read_sector(reader, i, &mut desc)?;
|
||||||
|
|
||||||
let desc_tag = u16::from_le_bytes([desc[0], desc[1]]);
|
let desc_tag = u16::from_le_bytes([desc[0], desc[1]]);
|
||||||
match desc_tag {
|
match desc_tag {
|
||||||
@@ -352,7 +352,7 @@ pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
|
|||||||
|
|
||||||
// Read LVD to check partition map type
|
// Read LVD to check partition map type
|
||||||
let mut lvd = [0u8; 2048];
|
let mut lvd = [0u8; 2048];
|
||||||
read_sector(session, lvd_sec, &mut lvd)?;
|
read_sector(reader, lvd_sec, &mut lvd)?;
|
||||||
|
|
||||||
// Parse partition maps starting at offset 440
|
// Parse partition maps starting at offset 440
|
||||||
// Map 0 = Type 1 (physical), Map 1 = Type 2 (metadata)
|
// Map 0 = Type 1 (physical), Map 1 = Type 2 (metadata)
|
||||||
@@ -368,7 +368,7 @@ pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
|
|||||||
// Read it to find where the metadata content starts
|
// Read it to find where the metadata content starts
|
||||||
let meta_file_lba = partition_start; // lba 0 of partition
|
let meta_file_lba = partition_start; // lba 0 of partition
|
||||||
let mut meta_icb = [0u8; 2048];
|
let mut meta_icb = [0u8; 2048];
|
||||||
read_sector(session, meta_file_lba, &mut meta_icb)?;
|
read_sector(reader, meta_file_lba, &mut meta_icb)?;
|
||||||
|
|
||||||
let meta_tag = u16::from_le_bytes([meta_icb[0], meta_icb[1]]);
|
let meta_tag = u16::from_le_bytes([meta_icb[0], meta_icb[1]]);
|
||||||
if meta_tag == 266 {
|
if meta_tag == 266 {
|
||||||
@@ -401,7 +401,7 @@ pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
|
|||||||
// Step 4: Read File Set Descriptor from metadata partition
|
// Step 4: Read File Set Descriptor from metadata partition
|
||||||
// FSD is at metadata-relative lba 0 (first sector of metadata content)
|
// FSD is at metadata-relative lba 0 (first sector of metadata content)
|
||||||
let mut fsd = [0u8; 2048];
|
let mut fsd = [0u8; 2048];
|
||||||
read_sector(session, metadata_start, &mut fsd)?;
|
read_sector(reader, metadata_start, &mut fsd)?;
|
||||||
|
|
||||||
let fsd_tag = u16::from_le_bytes([fsd[0], fsd[1]]);
|
let fsd_tag = u16::from_le_bytes([fsd[0], fsd[1]]);
|
||||||
if fsd_tag != 256 {
|
if fsd_tag != 256 {
|
||||||
@@ -413,7 +413,7 @@ pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
|
|||||||
let root_lba = u32::from_le_bytes([fsd[404], fsd[405], fsd[406], fsd[407]]);
|
let root_lba = u32::from_le_bytes([fsd[404], fsd[405], fsd[406], fsd[407]]);
|
||||||
|
|
||||||
// Step 5: Read root directory and build file tree
|
// Step 5: Read root directory and build file tree
|
||||||
let root = read_directory(session, partition_start, metadata_start, root_lba, "", 0)?;
|
let root = read_directory(reader, partition_start, metadata_start, root_lba, "", 0)?;
|
||||||
|
|
||||||
let metadata_sectors = (metadata_size_bytes + 2047) / 2048;
|
let metadata_sectors = (metadata_size_bytes + 2047) / 2048;
|
||||||
|
|
||||||
@@ -432,7 +432,7 @@ pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
|
|||||||
/// containing File Identifier Descriptors (FIDs). Each FID names a file/subdir
|
/// containing File Identifier Descriptors (FIDs). Each FID names a file/subdir
|
||||||
/// and points to its ICB.
|
/// and points to its ICB.
|
||||||
fn read_directory(
|
fn read_directory(
|
||||||
session: &mut DriveSession,
|
reader: &mut dyn SectorReader,
|
||||||
part_start: u32,
|
part_start: u32,
|
||||||
meta_start: u32,
|
meta_start: u32,
|
||||||
meta_lba: u32,
|
meta_lba: u32,
|
||||||
@@ -441,7 +441,7 @@ fn read_directory(
|
|||||||
) -> Result<DirEntry> {
|
) -> Result<DirEntry> {
|
||||||
// Read ICB for this directory
|
// Read ICB for this directory
|
||||||
let mut icb = [0u8; 2048];
|
let mut icb = [0u8; 2048];
|
||||||
read_sector(session, meta_start + meta_lba, &mut icb)?;
|
read_sector(reader, meta_start + meta_lba, &mut icb)?;
|
||||||
|
|
||||||
let tag = u16::from_le_bytes([icb[0], icb[1]]);
|
let tag = u16::from_le_bytes([icb[0], icb[1]]);
|
||||||
|
|
||||||
@@ -477,7 +477,7 @@ fn read_directory(
|
|||||||
let sector_count = ((ad_len + 2047) / 2048).min(64);
|
let sector_count = ((ad_len + 2047) / 2048).min(64);
|
||||||
let mut dir_data = vec![0u8; sector_count as usize * 2048];
|
let mut dir_data = vec![0u8; sector_count as usize * 2048];
|
||||||
for i in 0..sector_count {
|
for i in 0..sector_count {
|
||||||
read_sector(session, dir_abs + i,
|
read_sector(reader, dir_abs + i,
|
||||||
&mut dir_data[(i as usize) * 2048..(i as usize + 1) * 2048])?;
|
&mut dir_data[(i as usize) * 2048..(i as usize + 1) * 2048])?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,11 +512,11 @@ fn read_directory(
|
|||||||
|
|
||||||
if !entry_name.is_empty() {
|
if !entry_name.is_empty() {
|
||||||
// Read the ICB to get file size
|
// Read the ICB to get file size
|
||||||
let file_size = read_file_size(session, meta_start, icb_lba).unwrap_or(0);
|
let file_size = read_file_size(reader, meta_start, icb_lba).unwrap_or(0);
|
||||||
|
|
||||||
if is_dir && depth < 3 {
|
if is_dir && depth < 3 {
|
||||||
// Recurse into subdirectory (max 3 levels: BDMV/PLAYLIST/*.mpls)
|
// Recurse into subdirectory (max 3 levels: BDMV/PLAYLIST/*.mpls)
|
||||||
let subdir = read_directory(session, part_start, meta_start, icb_lba, &entry_name, depth + 1)?;
|
let subdir = read_directory(reader, part_start, meta_start, icb_lba, &entry_name, depth + 1)?;
|
||||||
entries.push(subdir);
|
entries.push(subdir);
|
||||||
} else {
|
} else {
|
||||||
entries.push(DirEntry {
|
entries.push(DirEntry {
|
||||||
@@ -545,9 +545,9 @@ fn read_directory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read file size (info_length) from an Extended File Entry ICB.
|
/// Read file size (info_length) from an Extended File Entry ICB.
|
||||||
fn read_file_size(session: &mut DriveSession, meta_start: u32, meta_lba: u32) -> Result<u64> {
|
fn read_file_size(reader: &mut dyn SectorReader, meta_start: u32, meta_lba: u32) -> Result<u64> {
|
||||||
let mut icb = [0u8; 2048];
|
let mut icb = [0u8; 2048];
|
||||||
read_sector(session, meta_start + meta_lba, &mut icb)?;
|
read_sector(reader, meta_start + meta_lba, &mut icb)?;
|
||||||
|
|
||||||
let tag = u16::from_le_bytes([icb[0], icb[1]]);
|
let tag = u16::from_le_bytes([icb[0], icb[1]]);
|
||||||
match tag {
|
match tag {
|
||||||
@@ -642,7 +642,7 @@ fn parse_dstring(data: &[u8]) -> String {
|
|||||||
|
|
||||||
/// Read a single 2048-byte sector from the drive.
|
/// Read a single 2048-byte sector from the drive.
|
||||||
/// Uses standard READ(10) — no unlock required.
|
/// Uses standard READ(10) — no unlock required.
|
||||||
fn read_sector(session: &mut DriveSession, lba: u32, buf: &mut [u8]) -> Result<()> {
|
fn read_sector(reader: &mut dyn SectorReader, lba: u32, buf: &mut [u8]) -> Result<()> {
|
||||||
session.read_disc(lba, 1, buf)?;
|
reader.read_sectors(lba, 1, buf)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user