UDF: file data on physical partition, disc API with typed streams
This commit is contained in:
+262
-121
@@ -1,52 +1,67 @@
|
|||||||
//! UDF filesystem reader — read files from Blu-ray discs.
|
//! UDF filesystem reader — read files from Blu-ray discs.
|
||||||
//!
|
//!
|
||||||
//! Minimal UDF implementation: just enough to find and read files
|
//! Blu-ray discs use UDF 2.50 with metadata partitions.
|
||||||
//! in the BDMV directory structure. Not a full UDF implementation.
|
//! The read sequence follows pointers through the disc structure:
|
||||||
//!
|
//!
|
||||||
//! Reference: ECMA-167, UDF 2.50 (OSTA)
|
//! Sector 256 (AVDP)
|
||||||
|
//! → VDS (Partition Descriptor + Logical Volume Descriptor)
|
||||||
|
//! → Metadata Partition (virtual partition stored as a file)
|
||||||
|
//! → File Set Descriptor (FSD)
|
||||||
|
//! → Root Directory ICB
|
||||||
|
//! → Directory data (File Identifier Descriptors)
|
||||||
|
//! → BDMV/PLAYLIST/*.mpls, BDMV/CLIPINF/*.clpi
|
||||||
|
//!
|
||||||
|
//! Each step reads one or two sectors. No bulk reads needed.
|
||||||
|
//!
|
||||||
|
//! References:
|
||||||
|
//! ECMA-167 (UDF base)
|
||||||
|
//! UDF 2.50 (OSTA) — metadata partition extension
|
||||||
|
//! BD-ROM Part 3 — Blu-ray filesystem profile
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::drive::DriveSession;
|
use crate::drive::DriveSession;
|
||||||
use crate::scsi::DataDirection;
|
|
||||||
|
|
||||||
/// A UDF filesystem parsed from disc.
|
/// A UDF filesystem parsed from disc.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct UdfFs {
|
pub struct UdfFs {
|
||||||
/// Root directory entries
|
/// Root directory with full tree
|
||||||
pub root: DirEntry,
|
pub root: DirEntry,
|
||||||
/// Partition start LBA
|
/// Physical partition start (absolute sector)
|
||||||
partition_start: u32,
|
partition_start: u32,
|
||||||
|
/// Metadata partition start (absolute sector)
|
||||||
|
/// For UDF 2.50 discs, all file/directory references use metadata-relative LBAs
|
||||||
|
metadata_start: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A directory entry (file or directory).
|
/// A directory or file entry.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DirEntry {
|
pub struct DirEntry {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub is_dir: bool,
|
pub is_dir: bool,
|
||||||
/// LBA of the file/directory data
|
/// LBA within the metadata partition (add metadata_start for absolute)
|
||||||
pub lba: u32,
|
pub meta_lba: u32,
|
||||||
/// Size in bytes
|
/// File size in bytes (from ICB info_length)
|
||||||
pub size: u32,
|
pub size: u64,
|
||||||
/// Child entries (if directory)
|
/// Child entries (if directory)
|
||||||
pub entries: Vec<DirEntry>,
|
pub entries: Vec<DirEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UdfFs {
|
impl UdfFs {
|
||||||
/// Find a directory by path (e.g. "/BDMV/PLAYLIST").
|
/// Find a directory by path (e.g. "/BDMV/PLAYLIST").
|
||||||
|
/// Path matching is case-insensitive.
|
||||||
pub fn find_dir(&self, path: &str) -> Option<&DirEntry> {
|
pub fn find_dir(&self, path: &str) -> Option<&DirEntry> {
|
||||||
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 {
|
for part in &parts {
|
||||||
current = current.entries.iter().find(|e| {
|
current = current.entries.iter().find(|e| {
|
||||||
e.is_dir && e.name.eq_ignore_ascii_case(part)
|
e.is_dir && e.name.eq_ignore_ascii_case(part)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(current)
|
Some(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a file by path, returning its contents.
|
/// Read a file by path, returning its raw bytes.
|
||||||
|
/// Reads sector by sector from disc — no buffering.
|
||||||
pub fn read_file(&self, session: &mut DriveSession, path: &str) -> Result<Vec<u8>> {
|
pub fn read_file(&self, session: &mut DriveSession, 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;
|
||||||
@@ -68,181 +83,306 @@ impl UdfFs {
|
|||||||
detail: format!("file not found: {}", path),
|
detail: format!("file not found: {}", path),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Read the file sectors
|
// Read the file's ICB to get its data extent
|
||||||
let sector_count = (entry.size + 2047) / 2048;
|
let (data_lba, data_len) = self.read_icb_extent(session, entry.meta_lba)?;
|
||||||
let mut data = vec![0u8; (sector_count * 2048) as usize];
|
|
||||||
|
// Read file data sector by sector
|
||||||
|
let sector_count = ((data_len as u64 + 2047) / 2048) as u32;
|
||||||
|
let mut data = vec![0u8; (sector_count as usize) * 2048];
|
||||||
|
let abs_start = self.meta_to_abs(data_lba);
|
||||||
|
|
||||||
for i in 0..sector_count {
|
for i in 0..sector_count {
|
||||||
let lba = self.partition_start + entry.lba + i;
|
let offset = (i as usize) * 2048;
|
||||||
let offset = (i * 2048) as usize;
|
read_sector(session, abs_start + i, &mut data[offset..offset + 2048])?;
|
||||||
read_sector(session, lba, &mut data[offset..offset + 2048])?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.truncate(entry.size as usize);
|
data.truncate(entry.size as usize);
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a metadata-partition-relative LBA to an absolute sector number.
|
||||||
|
fn meta_to_abs(&self, meta_lba: u32) -> u32 {
|
||||||
|
self.metadata_start + meta_lba
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read an Extended File Entry (tag 266) or File Entry (tag 261)
|
||||||
|
/// and return its first allocation extent: (data_lba, data_length).
|
||||||
|
/// The data_lba is metadata-relative.
|
||||||
|
fn read_icb_extent(&self, session: &mut DriveSession, meta_lba: u32) -> Result<(u32, u32)> {
|
||||||
|
let mut icb = [0u8; 2048];
|
||||||
|
read_sector(session, self.meta_to_abs(meta_lba), &mut icb)?;
|
||||||
|
|
||||||
|
let tag = u16::from_le_bytes([icb[0], icb[1]]);
|
||||||
|
|
||||||
|
// Get allocation descriptor offset based on ICB type
|
||||||
|
let ad_offset = match tag {
|
||||||
|
// Extended File Entry (UDF 2.50, used by BD-ROM)
|
||||||
|
// Layout: ... L_EA at [208:212], L_AD at [212:216], alloc descs at 216 + L_EA
|
||||||
|
266 => {
|
||||||
|
let l_ea = u32::from_le_bytes([icb[208], icb[209], icb[210], icb[211]]) as usize;
|
||||||
|
216 + l_ea
|
||||||
|
}
|
||||||
|
// Standard File Entry
|
||||||
|
// Layout: ... L_EA at [168:172], L_AD at [172:176], alloc descs at 176 + L_EA
|
||||||
|
261 => {
|
||||||
|
let l_ea = u32::from_le_bytes([icb[168], icb[169], icb[170], icb[171]]) as usize;
|
||||||
|
176 + l_ea
|
||||||
|
}
|
||||||
|
_ => return Err(Error::DiscError {
|
||||||
|
detail: format!("unexpected ICB tag {} at meta_lba {}", tag, meta_lba),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if ad_offset + 8 > 2048 {
|
||||||
|
return Err(Error::DiscError { detail: "ICB alloc desc out of range".into() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short Allocation Descriptor: extent_length(4) + extent_position(4)
|
||||||
|
// extent_length upper 2 bits = type (0=recorded, 1=allocated not recorded, 3=next extent)
|
||||||
|
let raw_len = u32::from_le_bytes([icb[ad_offset], icb[ad_offset + 1],
|
||||||
|
icb[ad_offset + 2], icb[ad_offset + 3]]);
|
||||||
|
let data_len = raw_len & 0x3FFFFFFF;
|
||||||
|
let data_lba = u32::from_le_bytes([icb[ad_offset + 4], icb[ad_offset + 5],
|
||||||
|
icb[ad_offset + 6], icb[ad_offset + 7]]);
|
||||||
|
|
||||||
|
Ok((data_lba, data_len))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the UDF filesystem from a disc.
|
/// Read the UDF filesystem from a Blu-ray disc.
|
||||||
|
///
|
||||||
|
/// Follows the UDF pointer chain:
|
||||||
|
/// 1. AVDP (sector 256) → VDS location
|
||||||
|
/// 2. VDS → Partition Descriptor (physical partition start)
|
||||||
|
/// → Logical Volume Descriptor (FSD location + partition maps)
|
||||||
|
/// 3. Metadata partition file → metadata content location
|
||||||
|
/// 4. FSD → root directory ICB
|
||||||
|
/// 5. Root directory → file tree
|
||||||
pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
|
pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
|
||||||
// UDF Anchor Volume Descriptor Pointer at sector 256
|
// Step 1: Anchor Volume Descriptor Pointer 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(session, 256, &mut avdp)?;
|
||||||
|
|
||||||
// Check descriptor tag (tag ID = 2 for 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 {
|
||||||
return Err(Error::DiscError { detail: format!("not UDF: tag {} at sector 256", tag_id) });
|
return Err(Error::DiscError {
|
||||||
|
detail: format!("AVDP: expected tag 2, got {} at sector 256", tag_id),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main VDS extent: bytes 16-23
|
// Main VDS extent location: bytes [16:20] = LBA, [20:24] = length
|
||||||
let mvds_lba = u32::from_le_bytes([avdp[16], avdp[17], avdp[18], avdp[19]]);
|
// (We use the VDS at sectors 32+, not the reserve copy at sector 32768+)
|
||||||
let mvds_len = u32::from_le_bytes([avdp[20], avdp[21], avdp[22], avdp[23]]);
|
|
||||||
|
|
||||||
// Read Volume Descriptor Sequence to find Partition Descriptor and Logical Volume Descriptor
|
// Step 2: Read Volume Descriptor Sequence (sectors 32-37 typically)
|
||||||
|
// Find Partition Descriptor (tag 5) and Logical Volume Descriptor (tag 6)
|
||||||
let mut partition_start: u32 = 0;
|
let mut partition_start: u32 = 0;
|
||||||
let mut root_icb_lba: u32 = 0;
|
let mut num_partition_maps: u32 = 0;
|
||||||
|
let mut lvd_sector: Option<u32> = None;
|
||||||
|
|
||||||
let mvds_sectors = (mvds_len + 2047) / 2048;
|
for i in 32..64 {
|
||||||
for i in 0..mvds_sectors.min(32) {
|
|
||||||
let mut desc = [0u8; 2048];
|
let mut desc = [0u8; 2048];
|
||||||
read_sector(session, mvds_lba + i, &mut desc)?;
|
read_sector(session, 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 {
|
||||||
|
// Partition Descriptor — tells us where the physical partition starts
|
||||||
5 => {
|
5 => {
|
||||||
// Partition Descriptor
|
|
||||||
partition_start = u32::from_le_bytes([desc[188], desc[189], desc[190], desc[191]]);
|
partition_start = u32::from_le_bytes([desc[188], desc[189], desc[190], desc[191]]);
|
||||||
}
|
}
|
||||||
|
// Logical Volume Descriptor — contains FSD location and partition maps
|
||||||
6 => {
|
6 => {
|
||||||
// Logical Volume Descriptor — contains root FSD location
|
num_partition_maps = u32::from_le_bytes([desc[268], desc[269], desc[270], desc[271]]);
|
||||||
// LV Contents Use at offset 248: extent of File Set Descriptor
|
lvd_sector = Some(i);
|
||||||
let fsd_lba = u32::from_le_bytes([desc[248], desc[249], desc[250], desc[251]]);
|
|
||||||
root_icb_lba = fsd_lba;
|
|
||||||
}
|
}
|
||||||
8 => break, // Terminating Descriptor
|
// Terminating Descriptor — end of VDS
|
||||||
|
8 => break,
|
||||||
_ => continue,
|
_ => continue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if partition_start == 0 {
|
if partition_start == 0 {
|
||||||
return Err(Error::DiscError { detail: "UDF: no partition descriptor found".into() });
|
return Err(Error::DiscError { detail: "UDF: no Partition Descriptor found".into() });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read File Set Descriptor to get root directory ICB
|
// Step 3: Parse partition maps from LVD to find metadata partition
|
||||||
|
// BD-ROM discs (UDF 2.50) use a metadata partition (Type 2 map with "*UDF Metadata Partition")
|
||||||
|
// The metadata file is stored at lba=0 of the physical partition
|
||||||
|
let metadata_start = if num_partition_maps >= 2 {
|
||||||
|
let lvd_sec = lvd_sector.ok_or_else(|| Error::DiscError {
|
||||||
|
detail: "UDF: no LVD found".into(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Read LVD to check partition map type
|
||||||
|
let mut lvd = [0u8; 2048];
|
||||||
|
read_sector(session, lvd_sec, &mut lvd)?;
|
||||||
|
|
||||||
|
// 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_len = lvd[441] as usize;
|
||||||
|
|
||||||
|
if pm1_len > 0 && 440 + pm1_len < 2048 {
|
||||||
|
let pm2_type = lvd[440 + pm1_len]; // Second map type
|
||||||
|
|
||||||
|
if pm2_type == 2 {
|
||||||
|
// Type 2 = metadata partition
|
||||||
|
// The metadata file ICB is at physical partition lba 0
|
||||||
|
// Read it to find where the metadata content starts
|
||||||
|
let meta_file_lba = partition_start; // lba 0 of partition
|
||||||
|
let mut meta_icb = [0u8; 2048];
|
||||||
|
read_sector(session, meta_file_lba, &mut meta_icb)?;
|
||||||
|
|
||||||
|
let meta_tag = u16::from_le_bytes([meta_icb[0], meta_icb[1]]);
|
||||||
|
if meta_tag == 266 {
|
||||||
|
// Extended File Entry — get allocation extent
|
||||||
|
let l_ea = u32::from_le_bytes([meta_icb[208], meta_icb[209],
|
||||||
|
meta_icb[210], meta_icb[211]]) as usize;
|
||||||
|
let ad_off = 216 + l_ea;
|
||||||
|
let _ad_len = u32::from_le_bytes([meta_icb[ad_off], meta_icb[ad_off + 1],
|
||||||
|
meta_icb[ad_off + 2], meta_icb[ad_off + 3]]) & 0x3FFFFFFF;
|
||||||
|
let ad_pos = u32::from_le_bytes([meta_icb[ad_off + 4], meta_icb[ad_off + 5],
|
||||||
|
meta_icb[ad_off + 6], meta_icb[ad_off + 7]]);
|
||||||
|
// Metadata content starts at partition_start + ad_pos
|
||||||
|
partition_start + ad_pos
|
||||||
|
} else {
|
||||||
|
// Fallback: no metadata partition, use physical partition directly
|
||||||
|
partition_start
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
partition_start
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
partition_start
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single partition map — no metadata partition (older UDF)
|
||||||
|
partition_start
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 4: Read File Set Descriptor from metadata partition
|
||||||
|
// 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, partition_start + root_icb_lba, &mut fsd)?;
|
read_sector(session, 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 {
|
||||||
return Err(Error::DiscError { detail: format!("UDF: expected FSD (256), got tag {}", fsd_tag) });
|
return Err(Error::DiscError {
|
||||||
|
detail: format!("FSD: expected tag 256, got {} at sector {}", fsd_tag, metadata_start),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root Directory ICB at offset 400 in FSD
|
// Root Directory ICB: long_ad at FSD offset 400
|
||||||
let root_dir_lba = u32::from_le_bytes([fsd[400], fsd[401], fsd[402], fsd[403]]);
|
// long_ad = extent_length(4) + extent_location: lba(4) + part_ref(2) + impl_use(6)
|
||||||
|
let root_lba = u32::from_le_bytes([fsd[404], fsd[405], fsd[406], fsd[407]]);
|
||||||
|
|
||||||
// Read root directory
|
// Step 5: Read root directory and build file tree
|
||||||
let root = read_directory(session, partition_start, root_dir_lba, "")?;
|
let root = read_directory(session, partition_start, metadata_start, root_lba, "", 0)?;
|
||||||
|
|
||||||
Ok(UdfFs {
|
Ok(UdfFs {
|
||||||
root,
|
root,
|
||||||
partition_start,
|
partition_start,
|
||||||
|
metadata_start,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a UDF directory and its immediate children.
|
/// Read a UDF directory and its children (up to max_depth levels).
|
||||||
fn read_directory(session: &mut DriveSession, part_start: u32, dir_lba: u32, name: &str) -> Result<DirEntry> {
|
///
|
||||||
// Read the ICB (Information Control Block) for this directory
|
/// Each directory is an ICB (Extended File Entry) pointing to directory data
|
||||||
|
/// containing File Identifier Descriptors (FIDs). Each FID names a file/subdir
|
||||||
|
/// and points to its ICB.
|
||||||
|
fn read_directory(
|
||||||
|
session: &mut DriveSession,
|
||||||
|
part_start: u32,
|
||||||
|
meta_start: u32,
|
||||||
|
meta_lba: u32,
|
||||||
|
name: &str,
|
||||||
|
depth: u32,
|
||||||
|
) -> Result<DirEntry> {
|
||||||
|
// Read ICB for this directory
|
||||||
let mut icb = [0u8; 2048];
|
let mut icb = [0u8; 2048];
|
||||||
read_sector(session, part_start + dir_lba, &mut icb)?;
|
read_sector(session, meta_start + meta_lba, &mut icb)?;
|
||||||
|
|
||||||
let icb_tag = u16::from_le_bytes([icb[0], icb[1]]);
|
let tag = u16::from_le_bytes([icb[0], icb[1]]);
|
||||||
|
|
||||||
// File Entry (tag 261) or Extended File Entry (tag 266)
|
// Get allocation extent: where the directory data lives
|
||||||
let (alloc_offset, alloc_len) = match icb_tag {
|
let (ad_len, ad_pos) = match tag {
|
||||||
261 => {
|
|
||||||
// File Entry
|
|
||||||
let l_ea = u32::from_le_bytes([icb[168], icb[169], icb[170], icb[171]]) as usize;
|
|
||||||
let l_ad = u32::from_le_bytes([icb[172], icb[173], icb[174], icb[175]]) as usize;
|
|
||||||
(176 + l_ea, l_ad)
|
|
||||||
}
|
|
||||||
266 => {
|
266 => {
|
||||||
// Extended File Entry
|
|
||||||
let l_ea = u32::from_le_bytes([icb[208], icb[209], icb[210], icb[211]]) as usize;
|
let l_ea = u32::from_le_bytes([icb[208], icb[209], icb[210], icb[211]]) as usize;
|
||||||
let l_ad = u32::from_le_bytes([icb[212], icb[213], icb[214], icb[215]]) as usize;
|
let ad_off = 216 + l_ea;
|
||||||
(216 + l_ea, l_ad)
|
let len = u32::from_le_bytes([icb[ad_off], icb[ad_off + 1],
|
||||||
|
icb[ad_off + 2], icb[ad_off + 3]]) & 0x3FFFFFFF;
|
||||||
|
let pos = u32::from_le_bytes([icb[ad_off + 4], icb[ad_off + 5],
|
||||||
|
icb[ad_off + 6], icb[ad_off + 7]]);
|
||||||
|
(len, pos)
|
||||||
|
}
|
||||||
|
261 => {
|
||||||
|
let l_ea = u32::from_le_bytes([icb[168], icb[169], icb[170], icb[171]]) as usize;
|
||||||
|
let ad_off = 176 + l_ea;
|
||||||
|
let len = u32::from_le_bytes([icb[ad_off], icb[ad_off + 1],
|
||||||
|
icb[ad_off + 2], icb[ad_off + 3]]) & 0x3FFFFFFF;
|
||||||
|
let pos = u32::from_le_bytes([icb[ad_off + 4], icb[ad_off + 5],
|
||||||
|
icb[ad_off + 6], icb[ad_off + 7]]);
|
||||||
|
(len, pos)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Ok(DirEntry {
|
return Ok(DirEntry {
|
||||||
name: name.to_string(),
|
name: name.to_string(), is_dir: true, meta_lba, size: 0, entries: Vec::new(),
|
||||||
is_dir: true,
|
|
||||||
lba: dir_lba,
|
|
||||||
size: 0,
|
|
||||||
entries: Vec::new(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse allocation descriptors to find directory data location
|
|
||||||
// Short Allocation Descriptor: 8 bytes (4 length + 4 position)
|
|
||||||
let data_lba = if alloc_offset + 8 <= icb.len() {
|
|
||||||
u32::from_le_bytes([icb[alloc_offset + 4], icb[alloc_offset + 5],
|
|
||||||
icb[alloc_offset + 6], icb[alloc_offset + 7]])
|
|
||||||
} else {
|
|
||||||
dir_lba + 1 // assume data follows ICB
|
|
||||||
};
|
|
||||||
|
|
||||||
let data_len = if alloc_offset + 4 <= icb.len() {
|
|
||||||
u32::from_le_bytes([icb[alloc_offset], icb[alloc_offset + 1],
|
|
||||||
icb[alloc_offset + 2], icb[alloc_offset + 3]]) & 0x3FFFFFFF
|
|
||||||
} else {
|
|
||||||
2048
|
|
||||||
};
|
|
||||||
|
|
||||||
// Read directory data
|
// Read directory data
|
||||||
let sectors = ((data_len + 2047) / 2048).min(64) as usize;
|
let dir_abs = meta_start + ad_pos;
|
||||||
let mut dir_data = vec![0u8; sectors * 2048];
|
let sector_count = ((ad_len + 2047) / 2048).min(64);
|
||||||
for i in 0..sectors {
|
let mut dir_data = vec![0u8; sector_count as usize * 2048];
|
||||||
read_sector(session, part_start + data_lba + i as u32,
|
for i in 0..sector_count {
|
||||||
&mut dir_data[i * 2048..(i + 1) * 2048])?;
|
read_sector(session, dir_abs + i,
|
||||||
|
&mut dir_data[(i as usize) * 2048..(i as usize + 1) * 2048])?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse File Identifier Descriptors
|
// Parse File Identifier Descriptors
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
|
|
||||||
while pos + 38 < dir_data.len().min(data_len as usize) {
|
while pos + 38 < dir_data.len().min(ad_len as usize) {
|
||||||
let fid_tag = u16::from_le_bytes([dir_data[pos], dir_data[pos + 1]]);
|
let fid_tag = u16::from_le_bytes([dir_data[pos], dir_data[pos + 1]]);
|
||||||
if fid_tag != 257 {
|
if fid_tag != 257 {
|
||||||
break; // not a FID
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_chars = dir_data[pos + 18];
|
let file_chars = dir_data[pos + 18];
|
||||||
let l_fi = dir_data[pos + 19] as usize; // filename length
|
let l_fi = dir_data[pos + 19] as usize;
|
||||||
let icb_lba = u32::from_le_bytes([dir_data[pos + 20], dir_data[pos + 21],
|
|
||||||
dir_data[pos + 22], dir_data[pos + 23]]);
|
// FID ICB is a long_ad starting at offset 20:
|
||||||
|
// [20:24] = extent_length
|
||||||
|
// [24:28] = extent_location (LBA within metadata partition)
|
||||||
|
// [28:30] = partition_reference_number
|
||||||
|
// [30:36] = implementation_use
|
||||||
|
let icb_lba = u32::from_le_bytes([dir_data[pos + 24], dir_data[pos + 25],
|
||||||
|
dir_data[pos + 26], dir_data[pos + 27]]);
|
||||||
let l_iu = u16::from_le_bytes([dir_data[pos + 36], dir_data[pos + 37]]) as usize;
|
let l_iu = u16::from_le_bytes([dir_data[pos + 36], dir_data[pos + 37]]) as usize;
|
||||||
|
|
||||||
let name_offset = pos + 38 + l_iu;
|
|
||||||
let is_dir = (file_chars & 0x02) != 0;
|
let is_dir = (file_chars & 0x02) != 0;
|
||||||
let is_parent = (file_chars & 0x08) != 0;
|
let is_parent = (file_chars & 0x08) != 0;
|
||||||
|
|
||||||
if !is_parent && l_fi > 0 && name_offset + l_fi <= dir_data.len() {
|
if !is_parent && l_fi > 0 {
|
||||||
let raw_name = &dir_data[name_offset..name_offset + l_fi];
|
let name_start = pos + 38 + l_iu;
|
||||||
let entry_name = parse_udf_name(raw_name);
|
let entry_name = parse_udf_name(&dir_data[name_start..name_start + l_fi]);
|
||||||
|
|
||||||
if !entry_name.is_empty() {
|
if !entry_name.is_empty() {
|
||||||
if is_dir {
|
// Read the ICB to get file size
|
||||||
// Recurse into subdirectory (max 2 levels deep for BDMV)
|
let file_size = read_file_size(session, meta_start, icb_lba).unwrap_or(0);
|
||||||
let subdir = read_directory(session, part_start, icb_lba, &entry_name)?;
|
|
||||||
|
if is_dir && depth < 3 {
|
||||||
|
// Recurse into subdirectory (max 3 levels: BDMV/PLAYLIST/*.mpls)
|
||||||
|
let subdir = read_directory(session, part_start, meta_start, icb_lba, &entry_name, depth + 1)?;
|
||||||
entries.push(subdir);
|
entries.push(subdir);
|
||||||
} else {
|
} else {
|
||||||
// Get file size from its ICB
|
|
||||||
let file_size = read_file_size(session, part_start, icb_lba).unwrap_or(0);
|
|
||||||
entries.push(DirEntry {
|
entries.push(DirEntry {
|
||||||
name: entry_name,
|
name: entry_name,
|
||||||
is_dir: false,
|
is_dir,
|
||||||
lba: icb_lba,
|
meta_lba: icb_lba,
|
||||||
size: file_size,
|
size: file_size,
|
||||||
entries: Vec::new(),
|
entries: Vec::new(),
|
||||||
});
|
});
|
||||||
@@ -251,41 +391,41 @@ fn read_directory(session: &mut DriveSession, part_start: u32, dir_lba: u32, nam
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Advance to next FID (4-byte aligned)
|
// Advance to next FID (4-byte aligned)
|
||||||
let fid_len = 38 + l_iu + l_fi;
|
let fid_len = ((38 + l_iu + l_fi + 3) & !3) as usize;
|
||||||
let padded = (fid_len + 3) & !3;
|
pos += fid_len;
|
||||||
pos += padded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(DirEntry {
|
Ok(DirEntry {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
is_dir: true,
|
is_dir: true,
|
||||||
lba: dir_lba,
|
meta_lba,
|
||||||
size: data_len,
|
size: ad_len as u64,
|
||||||
entries,
|
entries,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read file size from a File Entry ICB.
|
/// Read file size (info_length) from an Extended File Entry ICB.
|
||||||
fn read_file_size(session: &mut DriveSession, part_start: u32, icb_lba: u32) -> Result<u32> {
|
fn read_file_size(session: &mut DriveSession, meta_start: u32, meta_lba: u32) -> Result<u64> {
|
||||||
let mut icb = [0u8; 2048];
|
let mut icb = [0u8; 2048];
|
||||||
read_sector(session, part_start + icb_lba, &mut icb)?;
|
read_sector(session, 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 {
|
||||||
261 => {
|
// Both File Entry (261) and Extended File Entry (266) have
|
||||||
// File Entry: info length at offset 56 (8 bytes LE)
|
// info_length as a u64 at offset 56
|
||||||
Ok(u32::from_le_bytes([icb[56], icb[57], icb[58], icb[59]]))
|
261 | 266 => {
|
||||||
}
|
Ok(u64::from_le_bytes([icb[56], icb[57], icb[58], icb[59],
|
||||||
266 => {
|
icb[60], icb[61], icb[62], icb[63]]))
|
||||||
// Extended File Entry: info length at offset 56
|
|
||||||
Ok(u32::from_le_bytes([icb[56], icb[57], icb[58], icb[59]]))
|
|
||||||
}
|
}
|
||||||
_ => Ok(0),
|
_ => Ok(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a UDF filename from raw bytes.
|
/// Parse a UDF filename from raw bytes.
|
||||||
/// UDF uses either 8-bit or 16-bit encoding (first byte = compression ID).
|
///
|
||||||
|
/// UDF uses a compression ID as the first byte:
|
||||||
|
/// 8 = 8-bit characters (ASCII)
|
||||||
|
/// 16 = 16-bit big-endian Unicode (UTF-16BE)
|
||||||
fn parse_udf_name(data: &[u8]) -> String {
|
fn parse_udf_name(data: &[u8]) -> String {
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
return String::new();
|
return String::new();
|
||||||
@@ -293,7 +433,7 @@ fn parse_udf_name(data: &[u8]) -> String {
|
|||||||
|
|
||||||
match data[0] {
|
match data[0] {
|
||||||
8 => {
|
8 => {
|
||||||
// 8-bit characters
|
// 8-bit ASCII
|
||||||
String::from_utf8_lossy(&data[1..]).trim().to_string()
|
String::from_utf8_lossy(&data[1..]).trim().to_string()
|
||||||
}
|
}
|
||||||
16 => {
|
16 => {
|
||||||
@@ -315,6 +455,7 @@ fn parse_udf_name(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.
|
||||||
fn read_sector(session: &mut DriveSession, lba: u32, buf: &mut [u8]) -> Result<()> {
|
fn read_sector(session: &mut DriveSession, lba: u32, buf: &mut [u8]) -> Result<()> {
|
||||||
session.read_disc(lba, 1, buf)?;
|
session.read_disc(lba, 1, buf)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user