use crate::tree::TreeNode;
use crate::Result;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
const SECTOR_SIZE: u64 = 2048;
#[derive(Debug, Clone, Copy)]
struct ExtentAd {
length: u32,
location: u32,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
struct LongAd {
length: u32,
location: u32,
partition: u16,
}
#[derive(Debug, Clone)]
struct PartitionInfo {
number: u16,
start_sector: u64,
}
#[derive(Debug, Clone)]
struct MetadataPartitionInfo {
file_location: u32,
partition_ref: u16,
}
#[derive(Debug, Clone)]
struct FileAllocation {
extents: Vec<ExtentAd>,
total_length: u64,
inline_data: Option<Vec<u8>>,
}
fn read_extent_ad(buffer: &[u8]) -> ExtentAd {
ExtentAd {
length: u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]),
location: u32::from_le_bytes([buffer[4], buffer[5], buffer[6], buffer[7]]),
}
}
fn read_long_ad(buffer: &[u8]) -> LongAd {
LongAd {
length: u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]),
location: u32::from_le_bytes([buffer[4], buffer[5], buffer[6], buffer[7]]),
partition: u16::from_le_bytes([buffer[8], buffer[9]]),
}
}
pub fn parse_udf(file: &mut File) -> Result<TreeNode> {
parse_udf_verbose(file, false)
}
pub fn parse_udf_verbose(file: &mut File, verbose: bool) -> Result<TreeNode> {
let mut found_udf_marker = false;
if verbose {
eprintln!("Scanning sectors 16-31 for UDF Volume Recognition Sequence...");
}
for sector in 16..32 {
if file.seek(SeekFrom::Start(sector * SECTOR_SIZE)).is_err() {
continue;
}
let mut buffer = [0u8; 16];
if file.read_exact(&mut buffer).is_err() {
continue;
}
let id = &buffer[1..6];
if id == b"NSR02" || id == b"NSR03" || id == b"BEA01" || id == b"TEA01" {
if verbose {
eprintln!(
" Found UDF marker '{:?}' at sector {}",
String::from_utf8_lossy(id),
sector
);
}
found_udf_marker = true;
break;
}
}
if !found_udf_marker {
return Err("Not a valid UDF filesystem (no VRS markers found)".into());
}
if verbose {
eprintln!("Looking for Anchor Volume Descriptor Pointer at sector 256...");
}
file.seek(SeekFrom::Start(256 * SECTOR_SIZE))?;
let mut avdp_buffer = [0u8; 512];
file.read_exact(&mut avdp_buffer)?;
let tag_id = u16::from_le_bytes([avdp_buffer[0], avdp_buffer[1]]);
if tag_id != 2 {
if verbose {
eprintln!(" AVDP not found at sector 256 (tag id: {})", tag_id);
}
return Err("UDF detected but AVDP not found at sector 256.".into());
}
let main_vds_extent = read_extent_ad(&avdp_buffer[16..24]);
if verbose {
eprintln!(
" Found AVDP. Main VDS at sector {}, length {}",
main_vds_extent.location, main_vds_extent.length
);
}
let mut partitions: Vec<PartitionInfo> = Vec::new();
let mut root_fsd_long_ad = None;
let mut metadata_partition: Option<MetadataPartitionInfo> = None;
let mut sector = main_vds_extent.location as u64;
let end_sector = sector + (main_vds_extent.length as u64).div_ceil(SECTOR_SIZE);
if verbose {
eprintln!(
"Parsing Main Volume Descriptor Sequence (sectors {} to {})...",
sector, end_sector
);
}
while sector < end_sector {
file.seek(SeekFrom::Start(sector * SECTOR_SIZE))?;
let mut vds_buffer = vec![0u8; SECTOR_SIZE as usize];
file.read_exact(&mut vds_buffer)?;
let vds_tag_id = u16::from_le_bytes([vds_buffer[0], vds_buffer[1]]);
match vds_tag_id {
5 => {
let part_num = u16::from_le_bytes([vds_buffer[22], vds_buffer[23]]);
let part_start = u32::from_le_bytes([
vds_buffer[188],
vds_buffer[189],
vds_buffer[190],
vds_buffer[191],
]) as u64;
if verbose {
eprintln!(
" Found Partition Descriptor #{}: starts at sector {}",
part_num, part_start
);
}
partitions.push(PartitionInfo {
number: part_num,
start_sector: part_start,
});
}
6 => {
root_fsd_long_ad = Some(read_long_ad(&vds_buffer[248..264]));
if verbose {
let ad = root_fsd_long_ad.unwrap();
eprintln!(
" Found Logical Volume Descriptor. FSD at location {} in partition {}",
ad.location, ad.partition
);
}
let map_table_length = u32::from_le_bytes([
vds_buffer[264],
vds_buffer[265],
vds_buffer[266],
vds_buffer[267],
]) as usize;
let num_partition_maps = u32::from_le_bytes([
vds_buffer[268],
vds_buffer[269],
vds_buffer[270],
vds_buffer[271],
]);
if verbose {
eprintln!(
" {} partition maps, table length {} bytes",
num_partition_maps, map_table_length
);
}
let mut map_offset = 440usize;
for map_idx in 0..num_partition_maps {
if map_offset + 2 > vds_buffer.len() {
break;
}
let map_type = vds_buffer[map_offset];
let map_length = vds_buffer[map_offset + 1] as usize;
if map_length == 0 {
break;
}
if verbose {
eprintln!(
" Partition map {}: type {}, length {}",
map_idx, map_type, map_length
);
}
if map_type == 2 && map_length >= 64 {
let id_string = &vds_buffer[map_offset + 5..map_offset + 28];
if verbose {
let id_printable: String = id_string
.iter()
.take_while(|&&b| b != 0)
.map(|&b| {
if (0x20..0x7f).contains(&b) {
b as char
} else {
'.'
}
})
.collect();
eprintln!(" Type 2 identifier: '{}'", id_printable);
}
if id_string.starts_with(b"*UDF Metadata Partition") {
let meta_part_ref = u16::from_le_bytes([
vds_buffer[map_offset + 38],
vds_buffer[map_offset + 39],
]);
let meta_file_loc = u32::from_le_bytes([
vds_buffer[map_offset + 40],
vds_buffer[map_offset + 41],
vds_buffer[map_offset + 42],
vds_buffer[map_offset + 43],
]);
if verbose {
eprintln!(
" Metadata Partition: file at location {} in partition {}",
meta_file_loc, meta_part_ref
);
}
metadata_partition = Some(MetadataPartitionInfo {
file_location: meta_file_loc,
partition_ref: meta_part_ref,
});
}
}
map_offset += map_length;
}
}
8 => {
if verbose {
eprintln!(" Found Terminating Descriptor at sector {}", sector);
}
break;
}
_ => {}
}
sector += 1;
}
let fsd_long_ad =
root_fsd_long_ad.ok_or("Failed to find File Set Descriptor location in LVD")?;
let fsd_partition_ref = fsd_long_ad.partition;
let (fsd_sector, partition_start) = if let Some(ref meta_info) = metadata_partition {
if verbose {
eprintln!("FSD is in metadata partition, reading via metadata file...");
}
let meta_phys_partition = partitions
.iter()
.find(|p| p.number == meta_info.partition_ref)
.ok_or("Cannot find physical partition for metadata file")?;
let meta_fe_sector = meta_phys_partition.start_sector + meta_info.file_location as u64;
if verbose {
eprintln!(" Metadata File Entry at sector {}", meta_fe_sector);
}
file.seek(SeekFrom::Start(meta_fe_sector * SECTOR_SIZE))?;
let mut meta_fe_buffer = vec![0u8; SECTOR_SIZE as usize];
file.read_exact(&mut meta_fe_buffer)?;
let meta_tag_id = u16::from_le_bytes([meta_fe_buffer[0], meta_fe_buffer[1]]);
if verbose {
eprintln!(" Metadata FE tag: {}", meta_tag_id);
}
let meta_alloc = get_file_allocation(&meta_fe_buffer)?;
let first_extent = meta_alloc
.extents
.first()
.ok_or("Metadata file has no allocation extents")?;
if verbose {
eprintln!(
" Metadata file extent: location {}, length {}",
first_extent.location, first_extent.length
);
}
let metadata_data_sector = meta_phys_partition.start_sector + first_extent.location as u64;
let fsd_offset_in_metadata = fsd_long_ad.location as u64;
(
metadata_data_sector + fsd_offset_in_metadata,
metadata_data_sector,
)
} else {
let partition = partitions
.iter()
.find(|p| p.number == fsd_partition_ref)
.or_else(|| partitions.first())
.ok_or("No partition found")?;
(
partition.start_sector + fsd_long_ad.location as u64,
partition.start_sector,
)
};
if verbose {
eprintln!("Reading File Set Descriptor at sector {}...", fsd_sector);
}
file.seek(SeekFrom::Start(fsd_sector * SECTOR_SIZE))?;
let mut fsd_buffer = [0u8; 512];
file.read_exact(&mut fsd_buffer)?;
let fsd_tag_id = u16::from_le_bytes([fsd_buffer[0], fsd_buffer[1]]);
if fsd_tag_id != 256 {
if verbose {
eprintln!(
" Tag {} at expected FSD location, scanning nearby...",
fsd_tag_id
);
}
let mut found_fsd = false;
for offset in 1..32 {
file.seek(SeekFrom::Start((fsd_sector + offset) * SECTOR_SIZE))?;
file.read_exact(&mut fsd_buffer)?;
let tag = u16::from_le_bytes([fsd_buffer[0], fsd_buffer[1]]);
if tag == 256 {
if verbose {
eprintln!(
" Found FSD at sector {} (offset +{})",
fsd_sector + offset,
offset
);
}
found_fsd = true;
break;
}
}
if !found_fsd {
return Err(format!(
"Invalid File Set Descriptor tag: expected 256, found {}",
fsd_tag_id
)
.into());
}
}
let root_icb_long_ad = read_long_ad(&fsd_buffer[400..416]);
if verbose {
eprintln!(
" Found FSD. Root ICB at location {} in partition {}",
root_icb_long_ad.location, root_icb_long_ad.partition
);
}
let mut root_node = TreeNode::new_directory("/".to_string());
if verbose {
eprintln!("Parsing root directory...");
}
parse_directory(
file,
partition_start,
&root_icb_long_ad,
&mut root_node,
verbose,
)?;
root_node.calculate_directory_size();
Ok(root_node)
}
fn get_file_allocation(fe_buffer: &[u8]) -> Result<FileAllocation> {
let tag_id = u16::from_le_bytes([fe_buffer[0], fe_buffer[1]]);
let (ad_length_offset, ea_length_offset, ad_data_offset_base) = match tag_id {
261 => (172, 168, 176usize), 266 => (212, 208, 216usize), _ => return Err(format!("Unsupported ICB tag: {}", tag_id).into()),
};
let icb_flags = u16::from_le_bytes([fe_buffer[18], fe_buffer[19]]);
let ad_type = icb_flags & 0x07;
let ea_length = u32::from_le_bytes([
fe_buffer[ea_length_offset],
fe_buffer[ea_length_offset + 1],
fe_buffer[ea_length_offset + 2],
fe_buffer[ea_length_offset + 3],
]) as usize;
let ad_length = u32::from_le_bytes([
fe_buffer[ad_length_offset],
fe_buffer[ad_length_offset + 1],
fe_buffer[ad_length_offset + 2],
fe_buffer[ad_length_offset + 3],
]) as usize;
let ad_offset = ad_data_offset_base + ea_length;
let mut extents = Vec::new();
let mut total_length: u64 = 0;
let mut inline_data = None;
match ad_type {
0 => {
let mut pos = ad_offset;
while pos + 8 <= fe_buffer.len() && pos < ad_offset + ad_length {
let raw_length = u32::from_le_bytes([
fe_buffer[pos],
fe_buffer[pos + 1],
fe_buffer[pos + 2],
fe_buffer[pos + 3],
]);
let extent_type = raw_length >> 30;
let length = raw_length & 0x3FFFFFFF;
let location = u32::from_le_bytes([
fe_buffer[pos + 4],
fe_buffer[pos + 5],
fe_buffer[pos + 6],
fe_buffer[pos + 7],
]);
if length == 0 {
break;
}
if extent_type == 3 {
break;
}
if extent_type == 0 {
extents.push(ExtentAd { length, location });
}
total_length += length as u64;
pos += 8;
}
}
1 => {
let mut pos = ad_offset;
while pos + 16 <= fe_buffer.len() && pos < ad_offset + ad_length {
let raw_length = u32::from_le_bytes([
fe_buffer[pos],
fe_buffer[pos + 1],
fe_buffer[pos + 2],
fe_buffer[pos + 3],
]);
let extent_type = raw_length >> 30;
let length = raw_length & 0x3FFFFFFF;
let location = u32::from_le_bytes([
fe_buffer[pos + 4],
fe_buffer[pos + 5],
fe_buffer[pos + 6],
fe_buffer[pos + 7],
]);
if length == 0 {
break;
}
if extent_type == 3 {
break;
}
if extent_type == 0 {
extents.push(ExtentAd { length, location });
}
total_length += length as u64;
pos += 16;
}
}
3 => {
let end = (ad_offset + ad_length).min(fe_buffer.len());
if ad_offset < end {
inline_data = Some(fe_buffer[ad_offset..end].to_vec());
total_length = (end - ad_offset) as u64;
}
}
_ => {
if ad_offset + 8 <= fe_buffer.len() {
let ext = read_extent_ad(&fe_buffer[ad_offset..ad_offset + 8]);
if ext.length > 0 {
total_length = ext.length as u64;
extents.push(ext);
}
}
}
}
if extents.is_empty() && inline_data.is_none() {
return Err("No allocation extents found in file entry".into());
}
Ok(FileAllocation {
extents,
total_length,
inline_data,
})
}
fn parse_directory(
file: &mut File,
partition_start: u64,
icb_long_ad: &LongAd,
parent_node: &mut TreeNode,
verbose: bool,
) -> Result<()> {
file.seek(SeekFrom::Start(
(partition_start + icb_long_ad.location as u64) * SECTOR_SIZE,
))?;
let mut fe_buffer = vec![0u8; SECTOR_SIZE as usize];
file.read_exact(&mut fe_buffer)?;
let alloc = get_file_allocation(&fe_buffer)?;
if verbose {
if alloc.inline_data.is_some() {
eprintln!(" Directory has inline data, {} bytes", alloc.total_length);
} else {
eprintln!(
" Directory has {} extent(s), total {} bytes",
alloc.extents.len(),
alloc.total_length
);
}
}
let buffer = if let Some(data) = alloc.inline_data {
data
} else {
let mut buf = Vec::with_capacity(alloc.total_length as usize);
for extent in &alloc.extents {
file.seek(SeekFrom::Start(
(partition_start + extent.location as u64) * SECTOR_SIZE,
))?;
let mut chunk = vec![0u8; extent.length as usize];
file.read_exact(&mut chunk)?;
buf.extend_from_slice(&chunk);
}
buf
};
let mut offset = 0;
while offset < buffer.len() {
if offset + 40 > buffer.len() {
break;
}
let tag_id = u16::from_le_bytes([buffer[offset], buffer[offset + 1]]);
if tag_id == 0 {
offset += 4;
continue;
}
if tag_id != 257 {
if verbose {
eprintln!(
" Warning: Expected FID (257) at offset {}, found {}",
offset, tag_id
);
}
break;
}
let file_characteristics = buffer[offset + 18];
let length_of_fi = buffer[offset + 19] as usize;
let icb = read_long_ad(&buffer[offset + 20..offset + 36]);
let length_of_iu = u16::from_le_bytes([buffer[offset + 36], buffer[offset + 37]]) as usize;
let name_offset = offset + 38 + length_of_iu;
if name_offset + length_of_fi > buffer.len() {
if verbose {
eprintln!(
" Warning: FID name offset out of bounds at offset {}",
offset
);
}
break;
}
let name = if length_of_fi == 0 {
String::new()
} else {
parse_udf_name(&buffer[name_offset..name_offset + length_of_fi])
};
let is_directory = (file_characteristics & 0x02) != 0;
let is_deleted = (file_characteristics & 0x04) != 0;
let is_parent = (file_characteristics & 0x08) != 0;
if !is_deleted && !is_parent && !name.is_empty() {
if verbose {
eprintln!(
" Found {}: {}",
if is_directory { "dir" } else { "file" },
name
);
}
if is_directory {
let mut dir_node = TreeNode::new_directory(name);
if let Err(e) = parse_directory(file, partition_start, &icb, &mut dir_node, verbose)
{
if verbose {
eprintln!(" Warning: Failed to parse subdirectory: {}", e);
}
}
parent_node.add_child(dir_node);
} else {
match get_file_info(file, partition_start, &icb) {
Ok(alloc) => {
let file_node = if let Some(first) = alloc.extents.first() {
TreeNode::new_file_with_location(
name,
alloc.total_length,
(partition_start + first.location as u64) * SECTOR_SIZE,
alloc.total_length,
)
} else {
TreeNode::new_file(name, alloc.total_length)
};
parent_node.add_child(file_node);
}
Err(e) => {
if verbose {
eprintln!(" Warning: Failed to get file extent: {}", e);
}
let file_node = TreeNode::new_file(name, 0);
parent_node.add_child(file_node);
}
}
}
}
let fid_length = 38 + length_of_iu + length_of_fi;
offset += (fid_length + 3) & !3;
}
Ok(())
}
fn get_file_info(
file: &mut File,
partition_start: u64,
icb_long_ad: &LongAd,
) -> Result<FileAllocation> {
file.seek(SeekFrom::Start(
(partition_start + icb_long_ad.location as u64) * SECTOR_SIZE,
))?;
let mut fe_buffer = vec![0u8; SECTOR_SIZE as usize];
file.read_exact(&mut fe_buffer)?;
get_file_allocation(&fe_buffer)
}
fn parse_udf_name(data: &[u8]) -> String {
if data.is_empty() {
return String::new();
}
let compression_id = data[0];
if compression_id == 8 {
String::from_utf8_lossy(&data[1..]).to_string()
} else if compression_id == 16 {
let utf16_data: Vec<u16> = data[1..]
.chunks_exact(2)
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
.collect();
String::from_utf16_lossy(&utf16_data)
} else {
String::from_utf8_lossy(data).to_string()
}
}