use crate::error::{Error, Result};
use crate::sector::SectorSource;
use std::collections::HashSet;
const MAX_FILE_BYTES: u64 = 64 * 1024 * 1024;
const MAX_DIR_BYTES: u32 = 1024 * 1024;
#[derive(Debug)]
pub struct UdfFs {
pub root: DirEntry,
pub volume_id: String,
partition_start: u32,
metadata_start: u32,
metadata_sectors: u32,
}
#[derive(Debug, Clone)]
pub struct DirEntry {
pub name: String,
pub is_dir: bool,
pub meta_lba: u32,
pub size: u64,
pub entries: Vec<DirEntry>,
}
impl UdfFs {
pub fn partition_start(&self) -> u32 {
self.partition_start
}
pub fn metadata_start(&self) -> u32 {
self.metadata_start
}
pub(crate) fn metadata_sectors(&self) -> u32 {
self.metadata_sectors
}
pub fn find_dir(&self, path: &str) -> Option<&DirEntry> {
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
let mut current = &self.root;
for part in &parts {
current = current
.entries
.iter()
.find(|e| e.is_dir && e.name.eq_ignore_ascii_case(part))?;
}
Some(current)
}
pub fn file_start_lba(&self, reader: &mut dyn SectorSource, path: &str) -> Result<u32> {
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
let mut current = &self.root;
for part in &parts[..parts.len() - 1] {
current = current
.entries
.iter()
.find(|e| e.is_dir && e.name.eq_ignore_ascii_case(part))
.ok_or_else(|| Error::UdfNotFound {
path: part.to_string(),
})?;
}
let filename = match parts.last() {
Some(f) => f,
None => {
return Err(Error::UdfNotFound {
path: path.to_string(),
});
}
};
let entry = current
.entries
.iter()
.find(|e| !e.is_dir && e.name.eq_ignore_ascii_case(filename))
.ok_or_else(|| Error::UdfNotFound {
path: path.to_string(),
})?;
let (data_lba, _) = self.read_icb_extent(reader, entry.meta_lba)?;
self.partition_start
.checked_add(data_lba)
.ok_or(Error::DiscRead {
sector: self.partition_start as u64,
status: None,
sense: None,
})
}
pub fn read_file(&self, reader: &mut dyn SectorSource, path: &str) -> Result<Vec<u8>> {
self.read_file_limited(reader, path, None)
}
pub fn read_file_prefix(
&self,
reader: &mut dyn SectorSource,
path: &str,
max_bytes: usize,
) -> Result<Vec<u8>> {
self.read_file_limited(reader, path, Some(max_bytes))
}
fn read_file_limited(
&self,
reader: &mut dyn SectorSource,
path: &str,
max_bytes: Option<usize>,
) -> Result<Vec<u8>> {
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
let mut current = &self.root;
for part in &parts[..parts.len() - 1] {
current = current
.entries
.iter()
.find(|e| e.is_dir && e.name.eq_ignore_ascii_case(part))
.ok_or_else(|| Error::UdfNotFound {
path: part.to_string(),
})?;
}
let filename = match parts.last() {
Some(f) => f,
None => {
return Err(Error::UdfNotFound {
path: path.to_string(),
});
}
};
let entry = current
.entries
.iter()
.find(|e| !e.is_dir && e.name.eq_ignore_ascii_case(filename))
.ok_or_else(|| Error::UdfNotFound {
path: path.to_string(),
})?;
let limit = max_bytes.unwrap_or(usize::MAX);
if let Some(mut inline) = self.read_inline_data(reader, entry.meta_lba)? {
let want = (entry.size as usize).min(limit);
if inline.len() > want {
inline.truncate(want);
}
return Ok(inline);
}
let extents = self.read_icb_extents(reader, entry.meta_lba)?;
if max_bytes.is_none() && entry.size > MAX_FILE_BYTES {
return Err(Error::DiscRead {
sector: self.partition_start as u64,
status: None,
sense: None,
});
}
let cap_hint = (entry.size as usize)
.min(limit)
.min(MAX_FILE_BYTES as usize);
let mut data = Vec::with_capacity(cap_hint);
let mut sector = [0u8; 2048];
'extents: for (data_lba, data_len) in extents {
if max_bytes.is_none() {
if data.len() as u64 + data_len as u64 > MAX_FILE_BYTES
|| data_len as u64 > MAX_FILE_BYTES
{
return Err(Error::DiscRead {
sector: self.partition_start as u64,
status: None,
sense: None,
});
}
}
let abs_start = self
.partition_start
.checked_add(data_lba)
.ok_or(Error::DiscRead {
sector: self.partition_start as u64,
status: None,
sense: None,
})?;
let sector_count = (data_len as u64).div_ceil(2048) as u32;
for i in 0..sector_count {
if data.len() >= limit {
break 'extents;
}
let abs = abs_start.checked_add(i).ok_or(Error::DiscRead {
sector: abs_start as u64,
status: None,
sense: None,
})?;
read_sector(reader, abs, &mut sector)?;
data.extend_from_slice(§or);
}
}
let trim_to = (entry.size as usize).min(limit);
if data.len() > trim_to {
data.truncate(trim_to);
}
Ok(data)
}
pub fn metadata_sector_ranges(&self, reader: &mut dyn SectorSource) -> Result<Vec<(u32, u32)>> {
let mut ranges = Vec::new();
let meta_end = self.metadata_start.saturating_add(self.metadata_sectors);
ranges.push((0, meta_end));
self.collect_file_ranges(reader, &self.root, &mut ranges)?;
ranges.sort_by_key(|r| r.0);
let merged = merge_ranges(&ranges);
Ok(merged)
}
pub fn all_sector_ranges(&self, reader: &mut dyn SectorSource) -> Result<Vec<(u32, u32)>> {
let mut ranges = Vec::new();
let meta_end = self.metadata_start.saturating_add(self.metadata_sectors);
ranges.push((0, meta_end));
self.collect_all_file_ranges(reader, &self.root, &mut ranges)?;
ranges.sort_by_key(|r| r.0);
let merged = merge_ranges(&ranges);
Ok(merged)
}
fn collect_all_file_ranges(
&self,
reader: &mut dyn SectorSource,
entry: &DirEntry,
ranges: &mut Vec<(u32, u32)>,
) -> Result<()> {
for child in &entry.entries {
if child.is_dir {
self.collect_all_file_ranges(reader, child, ranges)?;
} else {
ranges.push((self.meta_to_abs(child.meta_lba)?, 1));
if let Ok(extents) = self.read_icb_extents(reader, child.meta_lba) {
for (data_lba, data_len) in extents {
let abs_start = match self.partition_start.checked_add(data_lba) {
Some(v) => v,
None => continue,
};
let sector_count = (data_len as u64).div_ceil(2048) as u32;
ranges.push((abs_start, sector_count));
}
}
}
}
Ok(())
}
fn collect_file_ranges(
&self,
reader: &mut dyn SectorSource,
entry: &DirEntry,
ranges: &mut Vec<(u32, u32)>,
) -> Result<()> {
for child in &entry.entries {
if child.is_dir {
if child.name.eq_ignore_ascii_case("STREAM") {
continue;
}
self.collect_file_ranges(reader, child, ranges)?;
} else {
ranges.push((self.meta_to_abs(child.meta_lba)?, 1));
if child.size > 50_000_000 {
continue;
}
if let Ok(extents) = self.read_icb_extents(reader, child.meta_lba) {
for (data_lba, data_len) in extents {
let abs_start = match self.partition_start.checked_add(data_lba) {
Some(v) => v,
None => continue,
};
let sector_count = (data_len as u64).div_ceil(2048) as u32;
ranges.push((abs_start, sector_count));
}
}
}
}
Ok(())
}
fn meta_to_abs(&self, meta_lba: u32) -> Result<u32> {
self.metadata_start
.checked_add(meta_lba)
.ok_or(Error::DiscRead {
sector: self.metadata_start as u64,
status: None,
sense: None,
})
}
fn read_icb_extent(&self, reader: &mut dyn SectorSource, meta_lba: u32) -> Result<(u32, u32)> {
let extents = self.read_icb_extents(reader, meta_lba)?;
extents.first().copied().ok_or(Error::DiscRead {
sector: self.meta_to_abs(meta_lba).unwrap_or(0) as u64,
status: None,
sense: None,
})
}
fn read_inline_data(
&self,
reader: &mut dyn SectorSource,
meta_lba: u32,
) -> Result<Option<Vec<u8>>> {
let icb_abs = self.meta_to_abs(meta_lba)?;
let mut icb = [0u8; 2048];
read_sector(reader, icb_abs, &mut icb)?;
let tag = u16::from_le_bytes([icb[0], icb[1]]);
let (ad_offset, l_ad) = match tag {
266 => {
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;
(216 + l_ea, l_ad)
}
261 => {
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)
}
_ => return Ok(None),
};
let icb_flags = u16::from_le_bytes([icb[34], icb[35]]);
if (icb_flags & 0x07) != 3 {
return Ok(None);
}
if ad_offset > icb.len() || ad_offset + l_ad > icb.len() {
return Err(Error::DiscRead {
sector: icb_abs as u64,
status: None,
sense: None,
});
}
Ok(Some(icb[ad_offset..ad_offset + l_ad].to_vec()))
}
fn read_icb_extents(
&self,
reader: &mut dyn SectorSource,
meta_lba: u32,
) -> Result<Vec<(u32, u32)>> {
let icb_abs = self.meta_to_abs(meta_lba)?;
let mut icb = [0u8; 2048];
read_sector(reader, icb_abs, &mut icb)?;
let tag = u16::from_le_bytes([icb[0], icb[1]]);
let (ad_offset, l_ad) = match tag {
266 => {
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_offset = 216 + l_ea;
if ad_offset + l_ad > icb.len() {
return Err(Error::DiscRead {
sector: icb_abs as u64,
status: None,
sense: None,
});
}
(ad_offset, l_ad)
}
261 => {
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;
let ad_offset = 176 + l_ea;
if ad_offset + l_ad > icb.len() {
return Err(Error::DiscRead {
sector: icb_abs as u64,
status: None,
sense: None,
});
}
(ad_offset, l_ad)
}
_ => {
return Err(Error::DiscRead {
sector: icb_abs as u64,
status: None,
sense: None,
});
}
};
let icb_flags = u16::from_le_bytes([icb[34], icb[35]]);
let ad_type = (icb_flags & 0x07) as usize;
let ad_size: usize = match ad_type {
0 => 8, 1 => 16, 2 => 20, _ => 8,
};
let mut extents = Vec::new();
let mut block = icb;
let mut ad_start = ad_offset;
let mut ad_bytes = l_ad;
const MAX_AD_BLOCKS: usize = 256;
for _ in 0..MAX_AD_BLOCKS {
let num_descriptors = ad_bytes / ad_size;
let mut next_block: Option<u32> = None;
for i in 0..num_descriptors {
let off = ad_start + i * ad_size;
if off + ad_size > block.len() {
break;
}
let raw_len = u32::from_le_bytes([
block[off],
block[off + 1],
block[off + 2],
block[off + 3],
]);
let extent_type = raw_len >> 30;
let data_len = raw_len & 0x3FFF_FFFF;
let lba_off = if ad_size == 20 { off + 12 } else { off + 4 };
let data_lba = u32::from_le_bytes([
block[lba_off],
block[lba_off + 1],
block[lba_off + 2],
block[lba_off + 3],
]);
match extent_type {
0 if data_len == 0 => break,
0 => extents.push((data_lba, data_len)),
1 => {} 3 => {
if data_len > 0 {
next_block = Some(data_lba);
}
break;
}
_ => break,
}
}
match next_block {
Some(cont_lba) => {
read_sector(reader, self.meta_to_abs(cont_lba)?, &mut block)?;
ad_start = 0;
ad_bytes = block.len();
}
None => break,
}
}
Ok(extents)
}
pub fn file_extents(
&self,
reader: &mut dyn SectorSource,
path: &str,
) -> Result<Vec<(u32, u32)>> {
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
let mut current = &self.root;
for part in &parts[..parts.len() - 1] {
current = current
.entries
.iter()
.find(|e| e.is_dir && e.name.eq_ignore_ascii_case(part))
.ok_or_else(|| Error::UdfNotFound {
path: part.to_string(),
})?;
}
let filename = match parts.last() {
Some(f) => f,
None => {
return Err(Error::UdfNotFound {
path: path.to_string(),
});
}
};
let entry = current
.entries
.iter()
.find(|e| !e.is_dir && e.name.eq_ignore_ascii_case(filename))
.ok_or_else(|| Error::UdfNotFound {
path: path.to_string(),
})?;
let alloc_extents = self.read_icb_extents(reader, entry.meta_lba)?;
let mut disc_extents = Vec::with_capacity(alloc_extents.len());
for (lba, byte_len) in alloc_extents {
let abs_lba = self
.partition_start
.checked_add(lba)
.ok_or(Error::DiscRead {
sector: self.partition_start as u64,
status: None,
sense: None,
})?;
let sectors = (byte_len as u64).div_ceil(2048) as u32;
disc_extents.push((abs_lba, sectors));
}
Ok(disc_extents)
}
}
pub fn read_filesystem(reader: &mut dyn SectorSource) -> Result<UdfFs> {
let mut avdp = [0u8; 2048];
read_sector(reader, 256, &mut avdp)?;
let tag_id = u16::from_le_bytes([avdp[0], avdp[1]]);
if tag_id != 2 {
return Err(Error::DiscRead {
sector: 256,
status: None,
sense: None,
});
}
let mut partition_start: u32 = 0;
let mut num_partition_maps: u32 = 0;
let mut lvd_sector: Option<u32> = None;
let mut volume_id = String::new();
let mut metadata_size_bytes: u32 = 0;
for i in 32..64 {
let mut desc = [0u8; 2048];
read_sector(reader, i, &mut desc)?;
let desc_tag = u16::from_le_bytes([desc[0], desc[1]]);
match desc_tag {
1 => {
volume_id = parse_dstring(&desc[24..56]);
}
5 => {
partition_start = u32::from_le_bytes([desc[188], desc[189], desc[190], desc[191]]);
}
6 => {
num_partition_maps =
u32::from_le_bytes([desc[268], desc[269], desc[270], desc[271]]);
lvd_sector = Some(i);
}
8 => break,
_ => continue,
}
}
if partition_start == 0 {
return Err(Error::DiscRead {
sector: 0,
status: None,
sense: None,
});
}
let metadata_start = if num_partition_maps >= 2 {
let lvd_sec = lvd_sector.ok_or(Error::DiscRead {
sector: 0,
status: None,
sense: None,
})?;
let mut lvd = [0u8; 2048];
read_sector(reader, lvd_sec, &mut lvd)?;
let _pm1_type = lvd[440]; let pm1_len = lvd[441] as usize;
if pm1_len > 0 && 440 + pm1_len < 2048 {
let pm2_type = lvd[440 + pm1_len];
if pm2_type == 2 {
let meta_file_lba = partition_start; let mut meta_icb = [0u8; 2048];
read_sector(reader, meta_file_lba, &mut meta_icb)?;
let meta_tag = u16::from_le_bytes([meta_icb[0], meta_icb[1]]);
if meta_tag == 266 {
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;
if ad_off + 8 > meta_icb.len() {
return Err(Error::DiscRead {
sector: meta_file_lba as u64,
status: None,
sense: None,
});
}
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],
]) & 0x3FFF_FFFF;
metadata_size_bytes = ad_len;
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],
]);
partition_start.checked_add(ad_pos).ok_or(Error::DiscRead {
sector: partition_start as u64,
status: None,
sense: None,
})?
} else {
partition_start
}
} else {
partition_start
}
} else {
partition_start
}
} else {
partition_start
};
let mut fsd = [0u8; 2048];
read_sector(reader, metadata_start, &mut fsd)?;
let fsd_tag = u16::from_le_bytes([fsd[0], fsd[1]]);
if fsd_tag != 256 {
return Err(Error::DiscRead {
sector: metadata_start as u64,
status: None,
sense: None,
});
}
let root_lba = u32::from_le_bytes([fsd[404], fsd[405], fsd[406], fsd[407]]);
let root_icb_key = ((metadata_start as u64) << 32) | root_lba as u64;
let mut visited: HashSet<u64> = HashSet::from([root_icb_key]);
let root = read_directory(
reader,
partition_start,
metadata_start,
root_lba,
"",
0,
&mut 0usize,
&mut visited,
)?;
let metadata_sectors = (metadata_size_bytes as u64).div_ceil(2048) as u32;
Ok(UdfFs {
root,
volume_id,
partition_start,
metadata_start,
metadata_sectors,
})
}
const MAX_DIR_DEPTH: u32 = 8;
const MAX_TOTAL_DIR_ENTRIES: usize = 100_000;
#[allow(clippy::only_used_in_recursion)]
#[allow(clippy::too_many_arguments)]
fn read_directory(
reader: &mut dyn SectorSource,
part_start: u32,
meta_start: u32,
meta_lba: u32,
name: &str,
depth: u32,
budget: &mut usize,
visited: &mut HashSet<u64>,
) -> Result<DirEntry> {
let icb_abs = meta_start.checked_add(meta_lba).ok_or(Error::DiscRead {
sector: meta_start as u64,
status: None,
sense: None,
})?;
let mut icb = [0u8; 2048];
read_sector(reader, icb_abs, &mut icb)?;
let tag = u16::from_le_bytes([icb[0], icb[1]]);
let (ad_len, ad_pos) = match tag {
266 => {
let l_ea = u32::from_le_bytes([icb[208], icb[209], icb[210], icb[211]]) as usize;
let ad_off = 216 + l_ea;
if ad_off + 8 > icb.len() {
return Err(Error::DiscRead {
sector: icb_abs as u64,
status: None,
sense: None,
});
}
let len = u32::from_le_bytes([
icb[ad_off],
icb[ad_off + 1],
icb[ad_off + 2],
icb[ad_off + 3],
]) & 0x3FFF_FFFF;
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;
if ad_off + 8 > icb.len() {
return Err(Error::DiscRead {
sector: icb_abs as u64,
status: None,
sense: None,
});
}
let len = u32::from_le_bytes([
icb[ad_off],
icb[ad_off + 1],
icb[ad_off + 2],
icb[ad_off + 3],
]) & 0x3FFF_FFFF;
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 {
name: name.to_string(),
is_dir: true,
meta_lba,
size: 0,
entries: Vec::new(),
});
}
};
if ad_len > MAX_DIR_BYTES {
return Err(Error::DiscRead {
sector: meta_start as u64,
status: None,
sense: None,
});
}
let dir_abs = meta_start.checked_add(ad_pos).ok_or(Error::DiscRead {
sector: meta_start as u64,
status: None,
sense: None,
})?;
let sector_count = ad_len.div_ceil(2048);
let mut dir_data = vec![0u8; sector_count as usize * 2048];
for i in 0..sector_count {
let abs = dir_abs.checked_add(i).ok_or(Error::DiscRead {
sector: dir_abs as u64,
status: None,
sense: None,
})?;
read_sector(
reader,
abs,
&mut dir_data[(i as usize) * 2048..(i as usize + 1) * 2048],
)?;
}
let mut entries = Vec::new();
let mut pos = 0;
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]]);
if fid_tag != 257 {
break;
}
let file_chars = dir_data[pos + 18];
let l_fi = dir_data[pos + 19] as usize;
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 is_dir = (file_chars & 0x02) != 0;
let is_parent = (file_chars & 0x08) != 0;
if !is_parent && l_fi > 0 {
let name_start = pos + 38 + l_iu;
let name_end = name_start + l_fi;
if name_end > dir_data.len() {
break;
}
let entry_name = parse_udf_name(&dir_data[name_start..name_end]);
if !entry_name.is_empty() {
*budget = budget.saturating_add(1);
if *budget > MAX_TOTAL_DIR_ENTRIES {
return Err(Error::DiscRead {
sector: meta_start as u64,
status: None,
sense: None,
});
}
let file_size = read_file_size(reader, meta_start, icb_lba).unwrap_or(0);
if is_dir && depth < MAX_DIR_DEPTH {
let icb_key = ((meta_start as u64) << 32) | icb_lba as u64;
if visited.contains(&icb_key) {
entries.push(DirEntry {
name: entry_name,
is_dir: true,
meta_lba: icb_lba,
size: file_size,
entries: Vec::new(),
});
} else {
visited.insert(icb_key);
let subdir = read_directory(
reader,
part_start,
meta_start,
icb_lba,
&entry_name,
depth + 1,
budget,
visited,
)?;
entries.push(subdir);
}
} else {
entries.push(DirEntry {
name: entry_name,
is_dir,
meta_lba: icb_lba,
size: file_size,
entries: Vec::new(),
});
}
}
}
let fid_len = (38 + l_iu + l_fi + 3) & !3;
pos += fid_len;
}
Ok(DirEntry {
name: name.to_string(),
is_dir: true,
meta_lba,
size: ad_len as u64,
entries,
})
}
fn read_file_size(reader: &mut dyn SectorSource, meta_start: u32, meta_lba: u32) -> Result<u64> {
let abs = meta_start.checked_add(meta_lba).ok_or(Error::DiscRead {
sector: meta_start as u64,
status: None,
sense: None,
})?;
let mut icb = [0u8; 2048];
read_sector(reader, abs, &mut icb)?;
let tag = u16::from_le_bytes([icb[0], icb[1]]);
match tag {
261 | 266 => Ok(u64::from_le_bytes([
icb[56], icb[57], icb[58], icb[59], icb[60], icb[61], icb[62], icb[63],
])),
_ => Ok(0),
}
}
fn parse_udf_name(data: &[u8]) -> String {
if data.is_empty() {
return String::new();
}
match data[0] {
8 => {
String::from_utf8_lossy(&data[1..]).trim().to_string()
}
16 => {
let mut s = String::new();
let chars = &data[1..];
for i in (0..chars.len()).step_by(2) {
if i + 1 < chars.len() {
let c = ((chars[i] as u16) << 8) | chars[i + 1] as u16;
if let Some(ch) = char::from_u32(c as u32) {
s.push(ch);
}
}
}
s.trim().to_string()
}
_ => String::from_utf8_lossy(&data[1..]).trim().to_string(),
}
}
fn merge_ranges(ranges: &[(u32, u32)]) -> Vec<(u32, u32)> {
if ranges.is_empty() {
return Vec::new();
}
let mut result = vec![ranges[0]];
for &(start, count) in &ranges[1..] {
let last = result.last_mut().unwrap();
let last_end = last.0.saturating_add(last.1);
if start <= last_end.saturating_add(1) {
let new_end = start.saturating_add(count).max(last_end);
last.1 = new_end - last.0;
} else {
result.push((start, count));
}
}
result
}
fn parse_dstring(data: &[u8]) -> String {
if data.is_empty() {
return String::new();
}
let len = *data.last().unwrap() as usize;
if len == 0 || len > data.len() {
return String::new();
}
let content = &data[..len];
if content.is_empty() {
return String::new();
}
match content[0] {
8 => String::from_utf8_lossy(&content[1..])
.trim_end_matches('\0')
.trim()
.to_string(),
16 => {
let mut s = String::new();
let chars = &content[1..];
for i in (0..chars.len()).step_by(2) {
if i + 1 < chars.len() {
let c = ((chars[i] as u16) << 8) | chars[i + 1] as u16;
if c != 0 {
if let Some(ch) = char::from_u32(c as u32) {
s.push(ch);
}
}
}
}
s.trim().to_string()
}
_ => String::from_utf8_lossy(&content[1..])
.trim_end_matches('\0')
.trim()
.to_string(),
}
}
pub(crate) struct BufferedSectorReader<'a> {
inner: &'a mut dyn SectorSource,
cache_start: u32,
cache: Vec<u8>,
cache_sectors: u32,
batch: u16,
prefetched: std::collections::HashMap<u32, Vec<u8>>,
}
impl<'a> BufferedSectorReader<'a> {
pub(crate) fn new(inner: &'a mut dyn SectorSource, batch: u16) -> Self {
Self {
inner,
cache_start: u32::MAX,
cache: Vec::new(),
cache_sectors: 0,
batch,
prefetched: std::collections::HashMap::new(),
}
}
}
impl BufferedSectorReader<'_> {
pub(crate) fn prefetch(&mut self, start_lba: u32, count: u32) {
let count = count.min(8192);
let total = count as usize * 2048;
self.cache.resize(total, 0);
let mut offset = 0u32;
while offset < count {
let batch = (count - offset).min(self.batch as u32) as u16;
let buf_off = offset as usize * 2048;
if self
.inner
.read_sectors(
start_lba + offset,
batch,
&mut self.cache[buf_off..buf_off + batch as usize * 2048],
true,
)
.is_err()
{
break;
}
offset += batch as u32;
}
self.cache_start = start_lba;
self.cache_sectors = offset;
}
pub(crate) fn prefetch_ranges(&mut self, ranges: &[(u32, u32)]) {
const MAX_PREFETCH_SECTORS: u64 = 512 * 1024;
let mut tmp = vec![0u8; self.batch as usize * 2048];
let total: u64 = ranges.iter().map(|&(_, c)| c as u64).sum();
let mut cached: u64 = 0;
let mut done: u64 = 0;
let mut hb = crate::progress::Heartbeat::new("udf_prefetch");
for &(start, count) in ranges {
let mut offset = 0u32;
while offset < count {
hb.tick(done, total);
let batch = (count - offset).min(self.batch as u32) as u16;
let bytes = batch as usize * 2048;
if self
.inner
.read_sectors(start + offset, batch, &mut tmp[..bytes], true)
.is_err()
{
break;
}
for i in 0..batch as u32 {
if cached >= MAX_PREFETCH_SECTORS {
return;
}
let s = i as usize * 2048;
self.prefetched
.insert(start + offset + i, tmp[s..s + 2048].to_vec());
cached += 1;
}
offset += batch as u32;
done += batch as u64;
}
}
}
}
impl SectorSource for BufferedSectorReader<'_> {
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> std::result::Result<usize, crate::error::Error> {
if count == 1 {
if buf.len() < 2048 {
return Err(crate::error::Error::UdfBufferTooSmall);
}
if let Some(data) = self.prefetched.get(&lba) {
buf[..2048].copy_from_slice(data);
return Ok(2048);
}
if lba >= self.cache_start && lba < self.cache_start + self.cache_sectors {
let offset = (lba - self.cache_start) as usize * 2048;
buf[..2048].copy_from_slice(&self.cache[offset..offset + 2048]);
return Ok(2048);
}
let block = self.batch;
self.cache.resize(block as usize * 2048, 0);
match self.inner.read_sectors(lba, block, &mut self.cache, true) {
Ok(_) => {
self.cache_start = lba;
self.cache_sectors = block as u32;
}
Err(_) => {
self.cache.resize(2048, 0);
self.inner.read_sectors(lba, 1, &mut self.cache, true)?;
self.cache_start = lba;
self.cache_sectors = 1;
}
}
buf[..2048].copy_from_slice(&self.cache[..2048]);
Ok(2048)
} else {
self.inner.read_sectors(lba, count, buf, true)
}
}
}
fn read_sector(reader: &mut dyn SectorSource, lba: u32, buf: &mut [u8]) -> Result<()> {
reader.read_sectors(lba, 1, buf, true)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
struct MapReader {
sectors: HashMap<u32, [u8; 2048]>,
}
impl MapReader {
fn new() -> Self {
Self {
sectors: HashMap::new(),
}
}
fn put(&mut self, lba: u32, data: [u8; 2048]) {
self.sectors.insert(lba, data);
}
}
impl SectorSource for MapReader {
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let need = count as usize * 2048;
if buf.len() < need {
return Err(Error::UdfBufferTooSmall);
}
for i in 0..count as u32 {
let off = i as usize * 2048;
let s = self.sectors.get(&(lba + i)).copied().unwrap_or([0u8; 2048]);
buf[off..off + 2048].copy_from_slice(&s);
}
Ok(need)
}
}
fn build_efe(info_length: u64, ads: &[(u32, u32, u32)]) -> [u8; 2048] {
let mut s = [0u8; 2048];
s[0..2].copy_from_slice(&266u16.to_le_bytes()); s[56..64].copy_from_slice(&info_length.to_le_bytes()); let l_ea: u32 = 0;
let l_ad: u32 = (ads.len() * 8) as u32;
s[208..212].copy_from_slice(&l_ea.to_le_bytes());
s[212..216].copy_from_slice(&l_ad.to_le_bytes());
let mut off = 216 + l_ea as usize;
for &(etype, dlen, dlba) in ads {
let raw_len = (etype << 30) | (dlen & 0x3FFF_FFFF);
s[off..off + 4].copy_from_slice(&raw_len.to_le_bytes());
s[off + 4..off + 8].copy_from_slice(&dlba.to_le_bytes());
off += 8;
}
s
}
fn build_efe_long(info_length: u64, ads: &[(u32, u32, u32)]) -> [u8; 2048] {
let mut s = [0u8; 2048];
s[0..2].copy_from_slice(&266u16.to_le_bytes()); s[34..36].copy_from_slice(&1u16.to_le_bytes());
s[56..64].copy_from_slice(&info_length.to_le_bytes());
let l_ea: u32 = 0;
let l_ad: u32 = (ads.len() * 16) as u32;
s[208..212].copy_from_slice(&l_ea.to_le_bytes());
s[212..216].copy_from_slice(&l_ad.to_le_bytes());
let mut off = 216 + l_ea as usize;
for &(etype, dlen, dlba) in ads {
let raw_len = (etype << 30) | (dlen & 0x3FFF_FFFF);
s[off..off + 4].copy_from_slice(&raw_len.to_le_bytes());
s[off + 4..off + 8].copy_from_slice(&dlba.to_le_bytes());
off += 16;
}
s
}
fn build_cont_block(ads: &[(u32, u32, u32)]) -> [u8; 2048] {
let mut s = [0u8; 2048];
let mut off = 0usize;
for &(etype, dlen, dlba) in ads {
let raw_len = (etype << 30) | (dlen & 0x3FFF_FFFF);
s[off..off + 4].copy_from_slice(&raw_len.to_le_bytes());
s[off + 4..off + 8].copy_from_slice(&dlba.to_le_bytes());
off += 8;
}
s
}
fn fs_with(part_start: u32, meta_start: u32, root: DirEntry) -> UdfFs {
UdfFs {
root,
volume_id: String::new(),
partition_start: part_start,
metadata_start: meta_start,
metadata_sectors: 0,
}
}
fn file_entry(name: &str, meta_lba: u32, size: u64) -> DirEntry {
DirEntry {
name: name.to_string(),
is_dir: false,
meta_lba,
size,
entries: Vec::new(),
}
}
#[test]
fn icb_extents_follow_type3_continuation() {
let part_start = 1000;
let meta_start = 100;
let icb = build_efe(
6144,
&[
(0, 4096, 10), (3, 2048, 50), ],
);
let cont = build_cont_block(&[(0, 2048, 20)]);
let mut reader = MapReader::new();
reader.put(meta_start + 5, icb);
reader.put(meta_start + 50, cont);
let fs = fs_with(part_start, meta_start, file_entry("X", 5, 6144));
let extents = fs.read_icb_extents(&mut reader, 5).expect("extents");
assert_eq!(extents, vec![(10, 4096), (20, 2048)]);
}
#[test]
fn icb_extents_long_ad_returns_all_extents_not_just_first() {
let icb = build_efe_long(
4 * 1_000_000_000,
&[
(0, 0x3FFF_F800, 100), (0, 0x3FFF_F800, 600_000), (0, 0x3FFF_F800, 1_100_000),
(0, 0x1000_0000, 1_600_000), ],
);
let mut reader = MapReader::new();
reader.put(5, icb);
let fs = fs_with(0, 0, file_entry("BIG", 5, 4 * 1_000_000_000));
let extents = fs.read_icb_extents(&mut reader, 5).expect("extents");
assert_eq!(
extents,
vec![
(100, 0x3FFF_F800),
(600_000, 0x3FFF_F800),
(1_100_000, 0x3FFF_F800),
(1_600_000, 0x1000_0000),
],
"Long-AD file must return ALL extents, not just the first"
);
}
#[test]
fn read_file_spans_multiple_extents() {
let part_start = 0;
let meta_start = 0;
let icb = build_efe(4096, &[(0, 2048, 10), (0, 2048, 30)]);
let mut reader = MapReader::new();
reader.put(5, icb);
reader.put(10, [0xAA; 2048]);
reader.put(30, [0xBB; 2048]);
let root = DirEntry {
name: String::new(),
is_dir: true,
meta_lba: 0,
size: 0,
entries: vec![file_entry("F", 5, 4096)],
};
let fs = fs_with(part_start, meta_start, root);
let data = fs.read_file(&mut reader, "/F").expect("read");
assert_eq!(data.len(), 4096);
assert!(data[..2048].iter().all(|&b| b == 0xAA));
assert!(data[2048..].iter().all(|&b| b == 0xBB));
}
#[test]
fn read_file_long_ad_returns_full_content_not_truncated() {
let icb = build_efe_long(6144, &[(0, 2048, 10), (0, 2048, 30), (0, 2048, 50)]);
let mut reader = MapReader::new();
reader.put(5, icb);
reader.put(10, [0xAA; 2048]);
reader.put(30, [0xBB; 2048]);
reader.put(50, [0xCC; 2048]);
let root = DirEntry {
name: String::new(),
is_dir: true,
meta_lba: 0,
size: 0,
entries: vec![file_entry("MKB", 5, 6144)],
};
let fs = fs_with(0, 0, root);
let data = fs.read_file(&mut reader, "/MKB").expect("read");
assert_eq!(
data.len(),
6144,
"Long-AD file must not truncate at extent #0"
);
assert!(data[..2048].iter().all(|&b| b == 0xAA));
assert!(data[2048..4096].iter().all(|&b| b == 0xBB));
assert!(data[4096..].iter().all(|&b| b == 0xCC));
}
#[test]
fn read_aacs_inputs_reads_long_ad_files_in_full() {
let aacs = DirEntry {
name: "AACS".to_string(),
is_dir: true,
meta_lba: 0,
size: 0,
entries: vec![
file_entry("Unit_Key_RO.inf", 5, 4096), file_entry("MKB_RO.inf", 7, 2048),
],
};
let root = DirEntry {
name: String::new(),
is_dir: true,
meta_lba: 0,
size: 0,
entries: vec![aacs],
};
let mut reader = MapReader::new();
reader.put(5, build_efe_long(4096, &[(0, 2048, 10), (0, 2048, 30)]));
reader.put(10, [0xAA; 2048]);
reader.put(30, [0xBB; 2048]);
reader.put(7, build_efe_long(2048, &[(0, 2048, 50)]));
reader.put(50, [0xCC; 2048]);
let fs = fs_with(0, 0, root);
let (inf, _mkb) = crate::disc::Disc::read_aacs_inputs_from_reader(&mut reader, &fs)
.expect("read_aacs_inputs must succeed for a Long-AD disc");
assert_eq!(
inf.len(),
4096,
"Unit_Key_RO.inf (Long-AD, multi-extent) must read in full — the \
pre-0.31.1 Short-AD parser truncated it to the first 2048-byte extent"
);
assert!(inf[..2048].iter().all(|&b| b == 0xAA));
assert!(inf[2048..].iter().all(|&b| b == 0xBB));
}
#[test]
fn merge_ranges_saturates_near_u32_max() {
let ranges = [(u32::MAX - 1, 2), (u32::MAX, 5)];
let merged = merge_ranges(&ranges);
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].0, u32::MAX - 1);
}
#[test]
fn buffered_reader_short_buf_errors_not_panics() {
let mut inner = MapReader::new();
inner.put(0, [0u8; 2048]);
let mut br = BufferedSectorReader::new(&mut inner, 8);
let mut tiny = [0u8; 100];
let err = br.read_sectors(0, 1, &mut tiny, true);
assert!(matches!(err, Err(Error::UdfBufferTooSmall)));
}
struct MemReader {
sectors: HashMap<u32, [u8; 2048]>,
}
impl MemReader {
fn new() -> Self {
Self {
sectors: HashMap::new(),
}
}
fn put(&mut self, lba: u32, sector: [u8; 2048]) {
self.sectors.insert(lba, sector);
}
}
impl SectorSource for MemReader {
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
for i in 0..count as u32 {
let off = i as usize * 2048;
let dst = &mut buf[off..off + 2048];
match self.sectors.get(&(lba + i)) {
Some(s) => dst.copy_from_slice(s),
None => dst.fill(0),
}
}
Ok(count as usize * 2048)
}
}
fn build_efe_icb(info_len: u64, data_len: u32, data_lba: u32) -> [u8; 2048] {
let mut icb = [0u8; 2048];
icb[0..2].copy_from_slice(&266u16.to_le_bytes());
icb[56..64].copy_from_slice(&info_len.to_le_bytes());
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
icb[212..216].copy_from_slice(&8u32.to_le_bytes());
icb[216..220].copy_from_slice(&(data_len & 0x3FFF_FFFF).to_le_bytes());
icb[220..224].copy_from_slice(&data_lba.to_le_bytes());
icb
}
fn fs_with_file(meta_lba: u32, size: u64) -> UdfFs {
UdfFs {
root: DirEntry {
name: String::new(),
is_dir: true,
meta_lba: 0,
size: 0,
entries: vec![DirEntry {
name: "F".to_string(),
is_dir: false,
meta_lba,
size,
entries: Vec::new(),
}],
},
volume_id: String::new(),
partition_start: 0,
metadata_start: 0,
metadata_sectors: 0,
}
}
#[test]
fn read_file_rejects_oversized_extent_before_allocating() {
let oversized = MAX_FILE_BYTES as u32 + 2048;
let icb = build_efe_icb(oversized as u64, oversized, 100);
let mut reader = MemReader::new();
reader.put(10, icb);
let fs = fs_with_file(10, oversized as u64);
let err = fs.read_file(&mut reader, "/F").unwrap_err();
assert!(matches!(err, Error::DiscRead { .. }));
}
fn build_efe_icb_multi(info_len: u64, ads: &[(u32, u32)]) -> [u8; 2048] {
let mut icb = [0u8; 2048];
icb[0..2].copy_from_slice(&266u16.to_le_bytes());
icb[56..64].copy_from_slice(&info_len.to_le_bytes());
let l_ad = (ads.len() * 8) as u32;
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
icb[212..216].copy_from_slice(&l_ad.to_le_bytes());
for (i, (data_len, data_lba)) in ads.iter().enumerate() {
let off = 216 + i * 8;
icb[off..off + 4].copy_from_slice(&(data_len & 0x3FFF_FFFF).to_le_bytes());
icb[off + 4..off + 8].copy_from_slice(&data_lba.to_le_bytes());
}
icb
}
#[test]
fn read_file_rejects_cumulative_extents_over_cap() {
let big = MAX_FILE_BYTES as u32;
let icb = build_efe_icb_multi(MAX_FILE_BYTES * 2, &[(2048, 100), (big, 200_000)]);
let mut reader = MemReader::new();
reader.put(10, icb);
let mut data_sector = [0u8; 2048];
data_sector[0] = 0xCD;
reader.put(100, data_sector);
let fs = fs_with_file(10, 2048);
let err = fs.read_file(&mut reader, "/F").unwrap_err();
assert!(matches!(err, Error::DiscRead { .. }));
}
#[test]
fn read_file_rejects_oversized_info_length() {
let icb = build_efe_icb(0, 2048, 100);
let mut reader = MemReader::new();
reader.put(10, icb);
let fs = fs_with_file(10, MAX_FILE_BYTES + 1);
let err = fs.read_file(&mut reader, "/F").unwrap_err();
assert!(matches!(err, Error::DiscRead { .. }));
}
#[test]
fn read_file_accepts_small_file() {
let icb = build_efe_icb(2048, 2048, 100);
let mut reader = MemReader::new();
reader.put(10, icb);
let mut data_sector = [0u8; 2048];
data_sector[0] = 0xAB;
reader.put(100, data_sector);
let fs = fs_with_file(10, 2048);
let data = fs
.read_file(&mut reader, "/F")
.expect("small file should read");
assert_eq!(data.len(), 2048);
assert_eq!(data[0], 0xAB);
}
#[test]
fn read_directory_rejects_oversized_dir_before_allocating() {
let oversized = MAX_DIR_BYTES + 2048;
let icb = build_efe_icb(oversized as u64, oversized, 50);
let mut reader = MemReader::new();
reader.put(5, icb);
let err = read_directory(&mut reader, 0, 0, 5, "DIR", 0, &mut 0, &mut HashSet::new())
.unwrap_err();
assert!(matches!(err, Error::DiscRead { .. }));
}
#[test]
fn read_directory_accepts_small_empty_dir() {
let icb = build_efe_icb(2048, 2048, 50);
let mut reader = MemReader::new();
reader.put(5, icb);
let dir = read_directory(&mut reader, 0, 0, 5, "DIR", 0, &mut 0, &mut HashSet::new())
.expect("small dir parses");
assert!(dir.entries.is_empty());
assert!(dir.is_dir);
}
fn build_efe_ext(info_length: u64, ads: &[(u32, u32, u32)]) -> [u8; 2048] {
let mut s = [0u8; 2048];
s[0..2].copy_from_slice(&266u16.to_le_bytes()); s[34..36].copy_from_slice(&2u16.to_le_bytes());
s[56..64].copy_from_slice(&info_length.to_le_bytes());
let l_ea: u32 = 0;
let l_ad: u32 = (ads.len() * 20) as u32;
s[208..212].copy_from_slice(&l_ea.to_le_bytes());
s[212..216].copy_from_slice(&l_ad.to_le_bytes());
let mut off = 216 + l_ea as usize;
for &(etype, dlen, dlba) in ads {
let raw_len = (etype << 30) | (dlen & 0x3FFF_FFFF);
s[off..off + 4].copy_from_slice(&raw_len.to_le_bytes());
s[off + 4..off + 8].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
s[off + 8..off + 12].copy_from_slice(&0xCAFE_BABEu32.to_le_bytes());
s[off + 12..off + 16].copy_from_slice(&dlba.to_le_bytes());
off += 20;
}
s
}
#[test]
fn icb_extents_extended_ad_uses_20byte_stride_and_lba_at_off12() {
let icb = build_efe_ext(3 * 2048, &[(0, 2048, 700), (0, 2048, 800), (0, 4096, 900)]);
let mut reader = MapReader::new();
reader.put(5, icb);
let fs = fs_with(0, 0, file_entry("EXT", 5, 3 * 2048));
let extents = fs.read_icb_extents(&mut reader, 5).expect("extents");
assert_eq!(extents, vec![(700, 2048), (800, 2048), (900, 4096)]);
}
#[test]
fn icb_extents_short_ad_type1_sparse_extent_is_skipped_not_emitted() {
let icb = build_efe(
6144,
&[
(0, 2048, 10), (1, 2048, 20), (0, 2048, 30), ],
);
let mut reader = MapReader::new();
reader.put(5, icb);
let fs = fs_with(0, 0, file_entry("SP", 5, 6144));
let extents = fs.read_icb_extents(&mut reader, 5).expect("extents");
assert_eq!(extents, vec![(10, 2048), (30, 2048)]);
}
#[test]
fn icb_extents_zero_length_type0_terminates_list() {
let icb = build_efe(
2048,
&[
(0, 2048, 10), (0, 0, 0), (0, 4096, 999), ],
);
let mut reader = MapReader::new();
reader.put(5, icb);
let fs = fs_with(0, 0, file_entry("T", 5, 2048));
let extents = fs.read_icb_extents(&mut reader, 5).expect("extents");
assert_eq!(
extents,
vec![(10, 2048)],
"parsing must stop at the zero-length terminator"
);
}
#[test]
fn icb_extents_continuation_loop_terminates_without_hang_or_panic() {
let icb = build_efe(2048, &[(0, 2048, 10), (3, 2048, 50)]);
let cont = build_cont_block(&[(0, 2048, 20), (3, 2048, 50)]);
let mut reader = MapReader::new();
reader.put(5, icb);
reader.put(50, cont);
let fs = fs_with(0, 0, file_entry("LOOP", 5, 2048));
let extents = fs.read_icb_extents(&mut reader, 5).expect("extents");
assert!(extents.len() < 1024, "continuation chain must be bounded");
assert_eq!(extents[0], (10, 2048));
assert_eq!(extents[1], (20, 2048));
}
#[test]
fn parse_udf_name_decodes_utf16be_compression_id_16() {
let raw = [16u8, 0x00, b'A', 0x00, b'Z'];
assert_eq!(parse_udf_name(&raw), "AZ");
}
#[test]
fn parse_udf_name_8bit_compression_id_8() {
let mut raw = vec![8u8];
raw.extend_from_slice(b"BDMV");
assert_eq!(parse_udf_name(&raw), "BDMV");
}
#[test]
fn read_directory_honors_l_iu_offset_for_fid_name() {
let mut dir = [0u8; 2048];
let l_iu: u16 = 4;
let mut name_bytes = vec![8u8]; name_bytes.extend_from_slice(b"CLPI");
let l_fi = name_bytes.len() as u8;
dir[0..2].copy_from_slice(&257u16.to_le_bytes());
dir[18] = 0x00; dir[19] = l_fi;
dir[24..28].copy_from_slice(&7u32.to_le_bytes()); dir[36..38].copy_from_slice(&l_iu.to_le_bytes());
dir[38..42].copy_from_slice(&[0xFF, 0xFE, 0xFD, 0xFC]); let name_start = 38 + l_iu as usize;
dir[name_start..name_start + name_bytes.len()].copy_from_slice(&name_bytes);
let dir_icb = build_efe_icb(2048, 2048, 60); let mut reader = MemReader::new();
reader.put(5, dir_icb);
reader.put(60, dir);
reader.put(7, build_efe_icb(123, 2048, 0));
let parsed = read_directory(&mut reader, 0, 0, 5, "ROOT", 0, &mut 0, &mut HashSet::new())
.expect("dir parses");
assert_eq!(parsed.entries.len(), 1, "exactly one FID entry");
assert_eq!(
parsed.entries[0].name, "CLPI",
"name must be read at 38+L_IU, not from impl_use bytes"
);
assert!(!parsed.entries[0].is_dir);
}
#[test]
fn read_directory_skips_parent_fid_entry() {
let mut dir = [0u8; 2048];
let mut name_bytes = vec![8u8];
name_bytes.extend_from_slice(b"PARENT");
let l_fi = name_bytes.len() as u8;
dir[0..2].copy_from_slice(&257u16.to_le_bytes());
dir[18] = 0x08 | 0x02; dir[19] = l_fi; dir[24..28].copy_from_slice(&9u32.to_le_bytes());
dir[36..38].copy_from_slice(&0u16.to_le_bytes()); dir[38..38 + name_bytes.len()].copy_from_slice(&name_bytes);
let dir_icb = build_efe_icb(2048, 2048, 60);
let mut reader = MemReader::new();
reader.put(5, dir_icb);
reader.put(60, dir);
reader.put(9, build_efe_icb(0, 2048, 0));
let parsed = read_directory(&mut reader, 0, 0, 5, "ROOT", 0, &mut 0, &mut HashSet::new())
.expect("dir parses");
assert!(
parsed.entries.is_empty(),
"the parent (..) FID must not be emitted even with a valid name"
);
}
#[test]
fn parse_dstring_length_byte_caps_content() {
let mut field = [0u8; 32];
field[0] = 8; field[1] = b'V';
field[2] = b'O';
field[3] = b'L';
field[10] = b'X'; *field.last_mut().unwrap() = 4; assert_eq!(parse_dstring(&field), "VOL");
}
#[test]
fn parse_dstring_oversized_length_byte_returns_empty_not_panic() {
let mut field = [0u8; 8];
field[0] = 8;
field[1] = b'A';
*field.last_mut().unwrap() = 200; assert_eq!(parse_dstring(&field), "");
}
#[test]
fn read_inline_data_rejects_oversized_lea() {
let mut icb = [0u8; 2048];
icb[0..2].copy_from_slice(&266u16.to_le_bytes());
icb[34..36].copy_from_slice(&3u16.to_le_bytes());
let l_ea: u32 = 2000;
let l_ad: u32 = 4;
icb[208..212].copy_from_slice(&l_ea.to_le_bytes());
icb[212..216].copy_from_slice(&l_ad.to_le_bytes());
let mut reader = MapReader::new();
reader.put(0, icb);
let fs = fs_with(0, 0, file_entry("inline", 0, l_ad as u64));
let result = fs.read_inline_data(&mut reader, 0);
assert!(
result.is_err(),
"oversized L_EA must return Err, not Ok(Some(empty vec))"
);
}
#[test]
fn prefetch_huge_count_is_capped() {
let mut inner = MapReader::new();
let mut br = BufferedSectorReader::new(&mut inner, 32);
br.prefetch(0, 262_144);
assert!(
br.cache.len() <= 8192 * 2048,
"prefetch cache exceeded cap: {} bytes",
br.cache.len()
);
}
fn build_dir_sector_with_file_fids(count: usize, icb_base: u32) -> [u8; 2048] {
let mut sector = [0u8; 2048];
let l_fi: u8 = 2;
let name: [u8; 2] = [8, b'A'];
let fid_stride = 40usize; let mut pos = 0;
for i in 0..count {
if pos + fid_stride > sector.len() {
break;
}
sector[pos..pos + 2].copy_from_slice(&257u16.to_le_bytes()); sector[pos + 18] = 0x00; sector[pos + 19] = l_fi;
let lba = icb_base.wrapping_add(i as u32);
sector[pos + 24..pos + 28].copy_from_slice(&lba.to_le_bytes());
sector[pos + 38..pos + 40].copy_from_slice(&name);
pos += fid_stride;
}
sector
}
#[test]
fn read_directory_budget_exceeded_returns_err() {
let dir_sector = build_dir_sector_with_file_fids(51, 200);
let dir_icb = build_efe_icb(2048, 2048, 50);
let mut reader = MemReader::new();
reader.put(5, dir_icb);
reader.put(50, dir_sector);
let mut budget: usize = MAX_TOTAL_DIR_ENTRIES - 10;
let err = read_directory(
&mut reader,
0,
0,
5,
"ROOT",
0,
&mut budget,
&mut HashSet::new(),
)
.unwrap_err();
assert!(
matches!(err, Error::DiscRead { .. }),
"budget exceeded must return DiscRead"
);
}
#[test]
fn read_directory_icb_cycle_does_not_recurse() {
let mut dir = [0u8; 2048];
let mut name_bytes = vec![8u8];
name_bytes.extend_from_slice(b"LOOP");
let l_fi = name_bytes.len() as u8;
dir[0..2].copy_from_slice(&257u16.to_le_bytes()); dir[18] = 0x02; dir[19] = l_fi;
dir[24..28].copy_from_slice(&5u32.to_le_bytes()); dir[38..38 + name_bytes.len()].copy_from_slice(&name_bytes);
let dir_icb = build_efe_icb(2048, 2048, 60);
let mut reader = MemReader::new();
reader.put(5, dir_icb);
reader.put(60, dir);
let mut visited: HashSet<u64> = HashSet::new();
let root_key: u64 = 5u64; visited.insert(root_key);
let parsed = read_directory(&mut reader, 0, 0, 5, "ROOT", 0, &mut 0, &mut visited)
.expect("cycle must not blow up");
assert_eq!(parsed.entries.len(), 1);
assert_eq!(parsed.entries[0].name, "LOOP");
assert!(parsed.entries[0].is_dir);
assert!(
parsed.entries[0].entries.is_empty(),
"cycle entry must be a leaf, not recursed"
);
}
}