use core::fmt;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::io::{self, BufRead, BufReader, Read, Seek, SeekFrom};
use std::path::PathBuf;
use std::sync::{Arc, Mutex, PoisonError};
use super::descriptor::{
Avdp, Fsd, Lvd, PartitionDescriptor, PartitionMap, Pvd, Vdp, physical_partition_start,
};
use super::fid::parse_directory;
use super::icb::{FileEntry, parse_allocation_area};
use super::{
Extent, ExtentAd, ExtentKind, LongAd, TAG_ALLOCATION_EXTENT, TAG_TERMINATING, Tag, as_offset,
u32_le,
};
use crate::error::{BdError, ScanError, ScanStage};
use crate::vfs::fs::glob_ci;
use crate::vfs::{BdDir, BdFile, ReadSeek, SearchOption};
const ANCHOR_SECTOR: u64 = 256;
const BOOTSTRAP_SECTOR_SIZE: u64 = 2048;
const MAX_VDS_SECTORS: u64 = 256;
const MAX_BLOCK_SIZE: u64 = 32 << 10;
#[derive(Debug, Clone, Copy)]
#[expect(
clippy::struct_field_names,
reason = "the shared `max_` prefix names these uniformly as the bounding caps"
)]
struct Limits {
max_nodes: usize,
max_dir_bytes: u64,
max_continuations: usize,
max_extents: usize,
max_depth: usize,
max_fsd_sectors: u64,
}
impl Limits {
const DEFAULT: Self = Self {
max_nodes: 1 << 20,
max_dir_bytes: 64 << 20,
max_continuations: 4096,
max_extents: 1 << 16,
max_depth: 1 << 10,
max_fsd_sectors: 256,
};
}
pub trait IsoReader: fmt::Debug + Send + Sync {
fn open(&self) -> io::Result<Box<dyn ReadSeek>>;
}
#[derive(Debug, Clone)]
pub struct PathIso {
path: PathBuf,
}
impl PathIso {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
}
impl IsoReader for PathIso {
fn open(&self) -> io::Result<Box<dyn ReadSeek>> {
Ok(Box::new(crate::vfs::fs::open_sequential(&self.path)?))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum PartitionLoc {
Physical {
start: u64,
},
Metadata {
phys_start: u64,
extents: Vec<Extent>,
mirror_extents: Vec<Extent>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Run {
src: Option<u64>,
len: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Content {
Embedded(Vec<u8>),
Runs(Vec<Run>),
}
#[derive(Debug, PartialEq, Eq)]
struct FileBody {
length: u64,
content: Content,
}
#[derive(Debug)]
struct Node {
name: String,
full_name: String,
parent: Option<usize>,
kind: NodeKind,
}
#[derive(Debug)]
enum NodeKind {
Dir,
File(Arc<FileBody>),
}
#[derive(Debug)]
struct UdfInner {
factory: Box<dyn IsoReader>,
nodes: Vec<Node>,
label: String,
resilient: bool,
errors: Mutex<Vec<ScanError>>,
}
#[derive(Debug)]
pub struct UdfSource {
inner: Arc<UdfInner>,
}
impl UdfSource {
pub fn open(factory: Box<dyn IsoReader>) -> Result<Self, BdError> {
Self::open_with(factory, false)
}
pub fn open_resilient(factory: Box<dyn IsoReader>) -> Result<Self, BdError> {
Self::open_with(factory, true)
}
fn open_with(factory: Box<dyn IsoReader>, resilient: bool) -> Result<Self, BdError> {
let mut reader = factory.open()?;
let limits = Limits::DEFAULT;
let volume = parse_volume(&mut *reader, limits)?;
let label = volume.label.clone();
let nodes = build_tree(&mut *reader, &volume, limits)?;
Ok(Self {
inner: Arc::new(UdfInner {
factory,
nodes,
label,
resilient,
errors: Mutex::new(Vec::new()),
}),
})
}
#[must_use]
pub fn take_errors(&self) -> Vec<ScanError> {
std::mem::take(&mut *self.inner.errors.lock().unwrap_or_else(PoisonError::into_inner))
}
#[must_use]
pub fn root(&self) -> UdfDir {
UdfDir {
inner: Arc::clone(&self.inner),
node: 0,
name: self.inner.label.clone(),
full_name: String::new(),
parent: None,
}
}
#[must_use]
pub fn volume_label(&self) -> &str {
&self.inner.label
}
pub fn read_label(factory: &dyn IsoReader) -> Result<String, BdError> {
let mut reader = factory.open()?;
Ok(parse_volume(&mut *reader, Limits::DEFAULT)?.label)
}
}
fn dir_at(inner: &Arc<UdfInner>, idx: usize) -> Option<UdfDir> {
let node = inner.nodes.get(idx)?;
match node.kind {
NodeKind::Dir => Some(UdfDir {
inner: Arc::clone(inner),
node: idx,
name: node.name.clone(),
full_name: node.full_name.clone(),
parent: node.parent,
}),
NodeKind::File(_) => None,
}
}
#[derive(Debug)]
pub struct UdfDir {
inner: Arc<UdfInner>,
node: usize,
name: String,
full_name: String,
parent: Option<usize>,
}
impl UdfDir {
fn children(&self) -> io::Result<Vec<usize>> {
match self.inner.nodes.get(self.node) {
Some(Node { kind: NodeKind::Dir, .. }) => {
Ok(child_indices(&self.inner.nodes, self.node))
}
_ => Err(io::Error::other("udf node is not a directory")),
}
}
fn file_at(&self, idx: usize) -> Option<UdfFile> {
let node = self.inner.nodes.get(idx)?;
match &node.kind {
NodeKind::File(body) => Some(UdfFile {
inner: Arc::clone(&self.inner),
name: node.name.clone(),
full_name: node.full_name.clone(),
extension: extension_of(&node.name),
body: Arc::clone(body),
}),
NodeKind::Dir => None,
}
}
fn collect_files(
&self,
pattern: &[u8],
option: SearchOption,
out: &mut Vec<Box<dyn BdFile>>,
) -> io::Result<()> {
self.children()?;
self.collect_from(self.node, pattern, option, out);
Ok(())
}
fn collect_from(
&self,
dir: usize,
pattern: &[u8],
option: SearchOption,
out: &mut Vec<Box<dyn BdFile>>,
) {
for idx in child_indices(&self.inner.nodes, dir) {
if let Some(file) = self.file_at(idx) {
if glob_ci(pattern, file.name().as_bytes()) {
out.push(Box::new(file));
}
} else if option == SearchOption::AllDirectories {
self.collect_from(idx, pattern, option, out);
}
}
}
}
fn child_indices(nodes: &[Node], dir: usize) -> Vec<usize> {
nodes.iter().enumerate().filter(|(_, n)| n.parent == Some(dir)).map(|(idx, _)| idx).collect()
}
impl BdDir for UdfDir {
fn name(&self) -> &str {
&self.name
}
fn full_name(&self) -> &str {
&self.full_name
}
fn parent(&self) -> Option<Box<dyn BdDir>> {
let pidx = self.parent?;
dir_at(&self.inner, pidx).map(|d| -> Box<dyn BdDir> { Box::new(d) })
}
fn get_files(&self) -> io::Result<Vec<Box<dyn BdFile>>> {
self.get_files_pattern("*")
}
fn get_files_pattern(&self, pattern: &str) -> io::Result<Vec<Box<dyn BdFile>>> {
self.get_files_pattern_option(pattern, SearchOption::TopDirectoryOnly)
}
fn get_files_pattern_option(
&self,
pattern: &str,
option: SearchOption,
) -> io::Result<Vec<Box<dyn BdFile>>> {
let mut out: Vec<Box<dyn BdFile>> = Vec::new();
self.collect_files(pattern.as_bytes(), option, &mut out)?;
Ok(out)
}
fn get_directories(&self) -> io::Result<Vec<Box<dyn BdDir>>> {
let mut out: Vec<Box<dyn BdDir>> = Vec::new();
for idx in self.children()? {
if let Some(dir) = dir_at(&self.inner, idx) {
out.push(Box::new(dir));
}
}
Ok(out)
}
}
#[derive(Debug)]
pub struct UdfFile {
inner: Arc<UdfInner>,
name: String,
full_name: String,
extension: String,
body: Arc<FileBody>,
}
impl BdFile for UdfFile {
fn name(&self) -> &str {
&self.name
}
fn full_name(&self) -> &str {
&self.full_name
}
fn extension(&self) -> &str {
&self.extension
}
fn length(&self) -> u64 {
self.body.length
}
fn is_dir(&self) -> bool {
false
}
fn open_read(&self) -> io::Result<Box<dyn ReadSeek>> {
match &self.body.content {
Content::Embedded(bytes) => Ok(Box::new(EmbeddedReader {
bytes: bytes.clone(),
length: self.body.length,
pos: 0,
})),
Content::Runs(runs) => {
let stream = self.inner.factory.open()?;
Ok(Box::new(UdfFileReader {
inner: Arc::clone(&self.inner),
name: self.name.clone(),
reported: false,
stream,
runs: runs.clone(),
length: self.body.length,
pos: 0,
}))
}
}
}
fn open_text(&self) -> io::Result<Box<dyn BufRead>> {
Ok(Box::new(BufReader::new(self.open_read()?)))
}
}
struct UdfFileReader {
inner: Arc<UdfInner>,
name: String,
reported: bool,
stream: Box<dyn ReadSeek>,
runs: Vec<Run>,
length: u64,
pos: u64,
}
impl UdfFileReader {
fn recover(
&mut self,
err: io::Error,
buf: &mut [u8],
want: u64,
want_us: usize,
) -> io::Result<usize> {
if !self.inner.resilient {
return Err(err);
}
if !self.reported {
self.reported = true;
self.inner.errors.lock().unwrap_or_else(PoisonError::into_inner).push(ScanError {
file: format!("{} @ byte {}", self.name, self.pos),
stage: ScanStage::SectorRead,
reason: BdError::Io(err),
});
}
for slot in buf.iter_mut().take(want_us) {
*slot = 0;
}
self.pos = self.pos.saturating_add(want);
Ok(want_us)
}
}
impl Read for UdfFileReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() || self.pos >= self.length {
return Ok(0);
}
let mut start: u64 = 0;
for run in &self.runs {
let end = start.saturating_add(run.len);
if self.pos < end {
let into_run = self.pos.saturating_sub(start);
let run_avail = run.len.saturating_sub(into_run);
let file_avail = self.length.saturating_sub(self.pos);
let cap = u64::try_from(buf.len()).unwrap_or(u64::MAX);
let want = run_avail.min(file_avail).min(cap);
let want_us = usize::try_from(want).unwrap_or(usize::MAX);
return if let Some(off) = run.src {
let outcome = off
.checked_add(into_run)
.ok_or_else(|| io::Error::other("udf physical byte offset overflow"))
.and_then(|phys| {
self.stream.seek(SeekFrom::Start(phys))?;
(&mut self.stream).take(want).read(buf)
});
match outcome {
Ok(read) => {
self.pos = self.pos.saturating_add(u64::try_from(read).unwrap_or(0));
Ok(read)
}
Err(err) => self.recover(err, buf, want, want_us),
}
} else {
for slot in buf.iter_mut().take(want_us) {
*slot = 0;
}
self.pos = self.pos.saturating_add(want);
Ok(want_us)
};
}
start = end;
}
Ok(0)
}
}
impl Seek for UdfFileReader {
fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
self.pos = seek_within(self.pos, self.length, from)?;
Ok(self.pos)
}
}
const fn offset_by(base: u64, delta: i64) -> Option<u64> {
let magnitude = delta.unsigned_abs();
if delta >= 0 { base.checked_add(magnitude) } else { base.checked_sub(magnitude) }
}
fn seek_within(pos: u64, length: u64, from: SeekFrom) -> io::Result<u64> {
let target = match from {
SeekFrom::Start(off) => Some(off),
SeekFrom::Current(delta) => offset_by(pos, delta),
SeekFrom::End(delta) => offset_by(length, delta),
};
target.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid udf seek"))
}
struct EmbeddedReader {
bytes: Vec<u8>,
length: u64,
pos: u64,
}
impl Read for EmbeddedReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let file_avail = self.length.saturating_sub(self.pos);
let cap = u64::try_from(buf.len()).unwrap_or(u64::MAX);
let want = file_avail.min(cap);
let want_us = usize::try_from(want).unwrap_or(usize::MAX);
let pos_us = usize::try_from(self.pos).unwrap_or(usize::MAX);
for (i, slot) in buf.iter_mut().take(want_us).enumerate() {
*slot = pos_us.checked_add(i).and_then(|p| self.bytes.get(p)).copied().unwrap_or(0);
}
self.pos = self.pos.saturating_add(want);
Ok(want_us)
}
}
impl Seek for EmbeddedReader {
fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
self.pos = seek_within(self.pos, self.length, from)?;
Ok(self.pos)
}
}
fn extension_of(name: &str) -> String {
match name.rsplit_once('.') {
Some((_, ext)) => format!(".{ext}"),
None => String::new(),
}
}
struct Volume {
block_size: u64,
locs: Vec<PartitionLoc>,
label: String,
root_icb: (u16, u32),
}
fn read_sector_io(reader: &mut dyn ReadSeek, sector: u64, block_size: u64) -> io::Result<Vec<u8>> {
let offset = sector
.checked_mul(block_size)
.ok_or_else(|| io::Error::other("udf sector byte offset overflow"))?;
reader.seek(SeekFrom::Start(offset))?;
let len = usize::try_from(block_size).unwrap_or(usize::MAX);
let mut buf = vec![0_u8; len];
reader.read_exact(&mut buf)?;
Ok(buf)
}
fn read_sector(
reader: &mut dyn ReadSeek,
sector: u64,
block_size: u64,
) -> Result<Vec<u8>, BdError> {
Ok(read_sector_io(reader, sector, block_size)?)
}
fn resolve_sector(
locs: &[PartitionLoc],
partition_ref: u16,
block: u64,
block_size: u64,
) -> Option<u64> {
match locs.get(usize::from(partition_ref))? {
PartitionLoc::Physical { start } => start.checked_add(block),
PartitionLoc::Metadata { phys_start, extents, .. } => {
metadata_sector(*phys_start, extents, block, block_size)
}
}
}
fn resolve_mirror_sector(
locs: &[PartitionLoc],
partition_ref: u16,
block: u64,
block_size: u64,
) -> Option<u64> {
match locs.get(usize::from(partition_ref))? {
PartitionLoc::Physical { .. } => None,
PartitionLoc::Metadata { phys_start, mirror_extents, .. } => {
metadata_sector(*phys_start, mirror_extents, block, block_size)
}
}
}
fn read_sector_with_mirror(
reader: &mut dyn ReadSeek,
locs: &[PartitionLoc],
partition_ref: u16,
block: u64,
sector: u64,
block_size: u64,
) -> io::Result<Vec<u8>> {
match read_sector_io(reader, sector, block_size) {
Ok(buf) => Ok(buf),
Err(err) => {
if let Some(alt) = resolve_mirror_sector(locs, partition_ref, block, block_size)
&& let Ok(buf) = read_sector_io(reader, alt, block_size)
{
return Ok(buf);
}
Err(err)
}
}
}
fn metadata_sector(
phys_start: u64,
extents: &[Extent],
block: u64,
block_size: u64,
) -> Option<u64> {
let mut consumed: u64 = 0;
for extent in extents {
let blocks = u64::from(extent.length).checked_div(block_size)?;
let next = consumed.saturating_add(blocks);
if block >= consumed && block < next {
let within = block.saturating_sub(consumed);
let phys_block = u64::from(extent.block).saturating_add(within);
return Some(phys_start.saturating_add(phys_block));
}
consumed = next;
}
None
}
fn locate_avdp(reader: &mut dyn ReadSeek) -> Result<Avdp, BdError> {
let primary_err = match read_sector(reader, ANCHOR_SECTOR, BOOTSTRAP_SECTOR_SIZE) {
Ok(anchor) => match Avdp::parse(&anchor) {
Some(avdp) => return Ok(avdp),
None => BdError::StructureNotFound,
},
Err(err) => err,
};
if let Ok(len) = reader.seek(SeekFrom::End(0)) {
let sectors = len.checked_div(BOOTSTRAP_SECTOR_SIZE).unwrap_or(0);
for candidate in [sectors.saturating_sub(1), sectors.saturating_sub(257)] {
if let Ok(buf) = read_sector_io(reader, candidate, BOOTSTRAP_SECTOR_SIZE)
&& let Some(avdp) = Avdp::parse(&buf)
{
return Ok(avdp);
}
}
}
Err(primary_err)
}
fn parse_volume(reader: &mut dyn ReadSeek, limits: Limits) -> Result<Volume, BdError> {
let avdp = locate_avdp(reader)?;
let scan = {
let usable = |s: &VdsScan| s.lvd.is_some() && !s.partitions.is_empty();
match walk_vds(reader, avdp.main_vds) {
Ok(scan) if usable(&scan) => scan,
main => match walk_vds(reader, avdp.reserve_vds) {
Ok(reserve) if usable(&reserve) => reserve,
_ => main?,
},
}
};
let pvd_label = scan.pvd.map(|(_, pvd)| pvd.volume_identifier);
let (_, lvd) = scan.lvd.ok_or(BdError::StructureNotFound)?;
let partitions: Vec<PartitionDescriptor> =
scan.partitions.into_values().map(|(_, pd)| pd).collect();
let label = if lvd.logical_volume_identifier.is_empty() {
pvd_label.unwrap_or_default()
} else {
lvd.logical_volume_identifier.clone()
};
let block_size = u64::from(lvd.logical_block_size);
if block_size == 0 || block_size > MAX_BLOCK_SIZE {
return Err(BdError::StructureNotFound);
}
let locs = resolve_partitions(reader, &lvd, &partitions, block_size, limits)?;
let fsd = find_fsd(reader, &locs, lvd.file_set_descriptor, block_size, limits)?;
let root = fsd.root_directory_icb.location;
Ok(Volume { block_size, locs, label, root_icb: (root.partition, root.block) })
}
fn find_fsd(
reader: &mut dyn ReadSeek,
locs: &[PartitionLoc],
fsd_ad: LongAd,
block_size: u64,
limits: Limits,
) -> Result<Fsd, BdError> {
let partition = fsd_ad.location.partition;
let first_block = u64::from(fsd_ad.location.block);
let extent_blocks = u64::from(fsd_ad.length_bytes()).div_ceil(block_size);
let blocks = extent_blocks.max(1).min(limits.max_fsd_sectors);
for offset in 0..blocks {
let block = first_block.saturating_add(offset);
let sector =
resolve_sector(locs, partition, block, block_size).ok_or(BdError::StructureNotFound)?;
let buf = read_sector_with_mirror(reader, locs, partition, block, sector, block_size)?;
if let Some(fsd) = Fsd::parse(&buf) {
return Ok(fsd);
}
if Tag::parse(&buf, 0).is_some_and(|t| t.identifier == TAG_TERMINATING) {
break;
}
}
Err(BdError::StructureNotFound)
}
struct VdsScan {
lvd: Option<(u32, Lvd)>,
pvd: Option<(u32, Pvd)>,
partitions: BTreeMap<u16, (u32, PartitionDescriptor)>,
}
fn walk_vds(reader: &mut dyn ReadSeek, extent: ExtentAd) -> Result<VdsScan, BdError> {
let mut scan = VdsScan { lvd: None, pvd: None, partitions: BTreeMap::new() };
let mut budget = MAX_VDS_SECTORS;
let mut sector = u64::from(extent.location);
let mut remaining = u64::from(extent.length).checked_div(BOOTSTRAP_SECTOR_SIZE).unwrap_or(0);
while remaining > 0 && budget > 0 {
budget = budget.saturating_sub(1);
remaining = remaining.saturating_sub(1);
let buf = read_sector(reader, sector, BOOTSTRAP_SECTOR_SIZE)?;
sector = sector.saturating_add(1);
let vdsn = u32_le(&buf, 16).unwrap_or(0);
if let Some(parsed) = Lvd::parse(&buf) {
if scan.lvd.as_ref().is_none_or(|(prev, _)| vdsn >= *prev) {
scan.lvd = Some((vdsn, parsed));
}
} else if let Some(parsed) = PartitionDescriptor::parse(&buf) {
let keep =
scan.partitions.get(&parsed.partition_number).is_none_or(|(prev, _)| vdsn >= *prev);
if keep {
scan.partitions.insert(parsed.partition_number, (vdsn, parsed));
}
} else if let Some(parsed) = Pvd::parse(&buf) {
if scan.pvd.as_ref().is_none_or(|(prev, _)| vdsn >= *prev) {
scan.pvd = Some((vdsn, parsed));
}
} else if let Some(vdp) = Vdp::parse(&buf) {
sector = u64::from(vdp.next.location);
remaining = u64::from(vdp.next.length).checked_div(BOOTSTRAP_SECTOR_SIZE).unwrap_or(0);
} else if Tag::parse(&buf, 0).is_some_and(|t| t.identifier == TAG_TERMINATING) {
break;
}
}
Ok(scan)
}
fn resolve_partitions(
reader: &mut dyn ReadSeek,
lvd: &Lvd,
partitions: &[PartitionDescriptor],
block_size: u64,
limits: Limits,
) -> Result<Vec<PartitionLoc>, BdError> {
let mut locs: Vec<PartitionLoc> = Vec::new();
for (index, map) in lvd.partition_maps.iter().enumerate() {
let partition_ref = u16::try_from(index).unwrap_or(u16::MAX);
let start = physical_partition_start(&lvd.partition_maps, partitions, partition_ref);
match map {
PartitionMap::Physical { .. } => {
let start = start.ok_or(BdError::StructureNotFound)?;
locs.push(PartitionLoc::Physical { start: u64::from(start) });
}
PartitionMap::Metadata(meta) => {
let phys_start = u64::from(start.ok_or(BdError::StructureNotFound)?);
let phys_ref = lvd
.partition_maps
.iter()
.position(|m| {
matches!(m, PartitionMap::Physical { partition_number }
if *partition_number == meta.physical_partition)
})
.and_then(|idx| u16::try_from(idx).ok())
.ok_or(BdError::StructureNotFound)?;
let primary = load_metadata_extents(
reader,
&locs,
phys_start.saturating_add(u64::from(meta.metadata_file_location)),
FILE_TYPE_METADATA,
phys_ref,
block_size,
limits,
);
let mirror = load_metadata_extents(
reader,
&locs,
phys_start.saturating_add(u64::from(meta.metadata_mirror_file_location)),
FILE_TYPE_METADATA_MIRROR,
phys_ref,
block_size,
limits,
);
let (extents, mirror_extents) = match (primary, mirror) {
(Some(p), Some(m)) => (p, m),
(Some(p), None) => (p, Vec::new()),
(None, Some(m)) => (m, Vec::new()),
(None, None) => return Err(BdError::StructureNotFound),
};
locs.push(PartitionLoc::Metadata { phys_start, extents, mirror_extents });
}
PartitionMap::Other { .. } => {
locs.push(PartitionLoc::Physical { start: 0 });
}
}
}
Ok(locs)
}
const FILE_TYPE_METADATA: u8 = 250;
const FILE_TYPE_METADATA_MIRROR: u8 = 251;
fn load_metadata_extents(
reader: &mut dyn ReadSeek,
locs: &[PartitionLoc],
fe_sector: u64,
expected_file_type: u8,
phys_ref: u16,
block_size: u64,
limits: Limits,
) -> Option<Vec<Extent>> {
let fe_buf = read_sector_io(reader, fe_sector, block_size).ok()?;
let fe = FileEntry::parse(&fe_buf)?;
if fe.icb_tag.file_type != expected_file_type {
return None;
}
let extents = collect_extents(reader, locs, &fe, phys_ref, block_size, limits).ok()?;
if extents.is_empty() {
return None;
}
Some(extents)
}
const AED_L_AD_OFFSET: usize = 20;
const AED_AD_START: usize = 24;
fn aed_allocation_area(area: &[u8]) -> Option<&[u8]> {
let tag = Tag::parse(area, 0)?;
if tag.identifier != TAG_ALLOCATION_EXTENT {
return None;
}
let l_ad = as_offset(u32_le(area, AED_L_AD_OFFSET)?);
let end = AED_AD_START.saturating_add(l_ad).min(area.len());
area.get(AED_AD_START..end)
}
fn collect_extents(
reader: &mut dyn ReadSeek,
locs: &[PartitionLoc],
fe: &FileEntry,
owning_ref: u16,
block_size: u64,
limits: Limits,
) -> Result<Vec<Extent>, BdError> {
let alloc_type = fe.icb_tag.allocation_type;
let mut out: Vec<Extent> = Vec::new();
let mut pending: VecDeque<Extent> =
fe.extents().iter().copied().take(limits.max_extents).collect();
let mut follows = 0_usize;
while let Some(extent) = pending.pop_front() {
if extent.kind == ExtentKind::NextExtent {
follows = follows.saturating_add(1);
if follows > limits.max_continuations {
break;
}
let area =
read_extent_bytes(reader, locs, owning_ref, &extent, block_size, block_size)?;
let Some(ads) = aed_allocation_area(&area) else { continue };
for more in parse_allocation_area(ads, alloc_type) {
if out.len().saturating_add(pending.len()) >= limits.max_extents {
break;
}
pending.push_back(more);
}
} else {
out.push(extent);
}
}
Ok(out)
}
fn extent_runs(
locs: &[PartitionLoc],
owning_ref: u16,
extent: &Extent,
block_size: u64,
) -> Option<Vec<Run>> {
let length = u64::from(extent.length);
if length == 0 {
return Some(Vec::new());
}
if matches!(extent.kind, ExtentKind::NotRecordedAllocated | ExtentKind::NotRecordedNotAllocated)
{
return Some(vec![Run { src: None, len: length }]);
}
let partition_ref = extent.partition_ref.unwrap_or(owning_ref);
match locs.get(usize::from(partition_ref))? {
PartitionLoc::Physical { start } => {
let sector = start.saturating_add(u64::from(extent.block));
let off = sector.saturating_mul(block_size);
Some(vec![Run { src: Some(off), len: length }])
}
PartitionLoc::Metadata { phys_start, extents, .. } => {
metadata_runs(*phys_start, extents, extent.block, length, block_size)
}
}
}
fn mirror_extent_runs(
locs: &[PartitionLoc],
owning_ref: u16,
extent: &Extent,
block_size: u64,
) -> Option<Vec<Run>> {
let length = u64::from(extent.length);
if length == 0
|| matches!(
extent.kind,
ExtentKind::NotRecordedAllocated | ExtentKind::NotRecordedNotAllocated
)
{
return None;
}
let partition_ref = extent.partition_ref.unwrap_or(owning_ref);
match locs.get(usize::from(partition_ref))? {
PartitionLoc::Physical { .. } => None,
PartitionLoc::Metadata { phys_start, mirror_extents, .. } => {
metadata_runs(*phys_start, mirror_extents, extent.block, length, block_size)
}
}
}
fn metadata_runs(
phys_start: u64,
extents: &[Extent],
start_block: u32,
length: u64,
block_size: u64,
) -> Option<Vec<Run>> {
if block_size == 0 {
return None;
}
let want_start = u64::from(start_block);
let want_end = want_start.saturating_add(length.div_ceil(block_size));
let mut runs: Vec<Run> = Vec::new();
let mut consumed: u64 = 0;
let mut emitted: u64 = 0;
for extent in extents {
let blocks = u64::from(extent.length).checked_div(block_size).unwrap_or(0);
let next = consumed.saturating_add(blocks);
let lo = want_start.max(consumed);
let hi = want_end.min(next);
if lo < hi {
let within = lo.saturating_sub(consumed);
let phys_block = u64::from(extent.block).saturating_add(within);
let off = phys_start.saturating_add(phys_block).saturating_mul(block_size);
let span = hi.saturating_sub(lo).saturating_mul(block_size);
let this = span.min(length.saturating_sub(emitted));
match runs.last_mut() {
Some(Run { src: Some(prev), len }) if prev.saturating_add(*len) == off => {
*len = len.saturating_add(this);
}
_ => runs.push(Run { src: Some(off), len: this }),
}
emitted = emitted.saturating_add(this);
}
consumed = next;
}
if emitted < length { None } else { Some(runs) }
}
fn read_extent_bytes(
reader: &mut dyn ReadSeek,
locs: &[PartitionLoc],
owning_ref: u16,
extent: &Extent,
block_size: u64,
cap: u64,
) -> Result<Vec<u8>, BdError> {
let cap32 = u32::try_from(cap).unwrap_or(u32::MAX);
let length = extent.length.min(cap32);
let clamped = Extent { length, ..*extent };
let runs =
extent_runs(locs, owning_ref, &clamped, block_size).ok_or(BdError::StructureNotFound)?;
match read_runs(reader, &runs, u64::from(length)) {
Ok(bytes) => Ok(bytes),
Err(err) => {
if let Some(mirror_runs) = mirror_extent_runs(locs, owning_ref, &clamped, block_size)
&& let Ok(bytes) = read_runs(reader, &mirror_runs, u64::from(length))
{
return Ok(bytes);
}
Err(err)
}
}
}
fn read_runs(reader: &mut dyn ReadSeek, runs: &[Run], cap: u64) -> Result<Vec<u8>, BdError> {
let mut out: Vec<u8> = Vec::new();
for run in runs {
let have = u64::try_from(out.len()).unwrap_or(u64::MAX);
let remaining = cap.saturating_sub(have);
if remaining == 0 {
break;
}
let take = run.len.min(remaining);
let take_us = usize::try_from(take).unwrap_or(usize::MAX);
match run.src {
Some(off) => {
reader.seek(SeekFrom::Start(off))?;
let mut buf = vec![0_u8; take_us];
reader.read_exact(&mut buf)?;
out.extend_from_slice(&buf);
}
None => out.extend(std::iter::repeat_n(0_u8, take_us)),
}
}
Ok(out)
}
fn build_tree(
reader: &mut dyn ReadSeek,
volume: &Volume,
limits: Limits,
) -> Result<Vec<Node>, BdError> {
let (root_ref, root_block) = volume.root_icb;
let mut nodes: Vec<Node> = vec![Node {
name: volume.label.clone(),
full_name: String::new(),
parent: None,
kind: NodeKind::Dir,
}];
let mut visited: BTreeSet<(u16, u32)> = BTreeSet::new();
visited.insert((root_ref, root_block));
let mut queue: VecDeque<(usize, u16, u32, String, usize)> = VecDeque::new();
queue.push_back((0, root_ref, root_block, String::new(), 0));
while let Some((dir_idx, dir_ref, dir_block, parent_path, depth)) = queue.pop_front() {
let children = expand_directory(reader, volume, &parent_path, dir_ref, dir_block, limits)?;
for child in children {
if nodes.len() >= limits.max_nodes {
break;
}
let is_dir = matches!(child.kind, ChildKind::Dir);
if is_dir && depth >= limits.max_depth {
continue;
}
let icb = (child.partition, child.block);
if !visited.insert(icb) {
continue;
}
let idx = nodes.len();
let child_path = child.full_name.clone();
nodes.push(Node {
name: child.name,
full_name: child.full_name,
parent: Some(dir_idx),
kind: match child.kind {
ChildKind::Dir => NodeKind::Dir,
ChildKind::File(body) => NodeKind::File(Arc::new(body)),
},
});
if is_dir {
queue.push_back((
idx,
child.partition,
child.block,
child_path,
depth.saturating_add(1),
));
}
}
}
Ok(nodes)
}
struct Child {
name: String,
full_name: String,
partition: u16,
block: u32,
kind: ChildKind,
}
enum ChildKind {
Dir,
File(FileBody),
}
fn expand_directory(
reader: &mut dyn ReadSeek,
volume: &Volume,
parent_path: &str,
dir_ref: u16,
dir_block: u32,
limits: Limits,
) -> Result<Vec<Child>, BdError> {
let block_size = volume.block_size;
let Some(sector) = resolve_sector(&volume.locs, dir_ref, u64::from(dir_block), block_size)
else {
return Ok(Vec::new());
};
let fe_buf = read_sector_with_mirror(
reader,
&volume.locs,
dir_ref,
u64::from(dir_block),
sector,
block_size,
)?;
let Some(fe) = FileEntry::parse(&fe_buf) else {
return Ok(Vec::new());
};
if !fe.is_directory() {
return Ok(Vec::new());
}
let dir_bytes = directory_bytes(reader, volume, &fe, dir_ref, limits)?;
let mut children: Vec<Child> = Vec::new();
for fid in parse_directory(&dir_bytes) {
if fid.is_parent() || fid.is_deleted() {
continue;
}
let child_ref = fid.icb.location.partition;
let child_block = fid.icb.location.block;
let Some(child_sector) =
resolve_sector(&volume.locs, child_ref, u64::from(child_block), block_size)
else {
continue;
};
let Ok(child_buf) = read_sector_with_mirror(
reader,
&volume.locs,
child_ref,
u64::from(child_block),
child_sector,
block_size,
) else {
continue;
};
let Some(child_fe) = FileEntry::parse(&child_buf) else {
continue;
};
let full_name = format!("{parent_path}/{}", fid.name);
let kind = if child_fe.is_directory() {
ChildKind::Dir
} else {
ChildKind::File(file_body(reader, volume, &child_fe, child_ref, limits)?)
};
children.push(Child {
name: fid.name,
full_name,
partition: child_ref,
block: child_block,
kind,
});
}
Ok(children)
}
fn directory_bytes(
reader: &mut dyn ReadSeek,
volume: &Volume,
fe: &FileEntry,
own_ref: u16,
limits: Limits,
) -> Result<Vec<u8>, BdError> {
if let Some(embedded) = fe.embedded_data() {
return Ok(embedded.to_vec());
}
let extents = collect_extents(reader, &volume.locs, fe, own_ref, volume.block_size, limits)?;
let mut bytes: Vec<u8> = Vec::new();
for extent in &extents {
let have = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
let remaining = limits.max_dir_bytes.saturating_sub(have);
if remaining == 0 {
break;
}
let chunk =
read_extent_bytes(reader, &volume.locs, own_ref, extent, volume.block_size, remaining)?;
bytes.extend_from_slice(&chunk);
}
Ok(bytes)
}
fn file_body(
reader: &mut dyn ReadSeek,
volume: &Volume,
fe: &FileEntry,
own_ref: u16,
limits: Limits,
) -> Result<FileBody, BdError> {
let length = fe.information_length;
if let Some(embedded) = fe.embedded_data() {
return Ok(FileBody { length, content: Content::Embedded(embedded.to_vec()) });
}
let extents = collect_extents(reader, &volume.locs, fe, own_ref, volume.block_size, limits)?;
let mut runs: Vec<Run> = Vec::new();
for extent in &extents {
if let Some(mut resolved) = extent_runs(&volume.locs, own_ref, extent, volume.block_size) {
let room = limits.max_extents.saturating_sub(runs.len());
resolved.truncate(room);
runs.append(&mut resolved);
}
}
Ok(FileBody { length, content: Content::Runs(runs) })
}
#[cfg(test)]
#[expect(
clippy::arithmetic_side_effects,
clippy::cast_possible_truncation,
clippy::as_conversions,
reason = "test scaffolding builds .iso byte layouts with controlled, in-range offsets/sizes"
)]
mod tests {
use std::io::{Read, Seek, SeekFrom, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use proptest::prelude::{any, prop_assert_eq, proptest};
use super::{
BdDir, BdFile, Content, EmbeddedReader, ExtentKind, FileEntry, IsoReader, Limits, Node,
NodeKind, PartitionLoc, PathIso, Run, UdfDir, UdfFileReader, UdfInner, UdfSource, Volume,
build_tree, collect_extents, directory_bytes, expand_directory, extension_of, extent_runs,
file_body, metadata_runs, metadata_sector, offset_by, parse_volume, read_runs,
resolve_partitions, resolve_sector,
};
use crate::error::ScanStage;
use crate::vfs::SearchOption;
use crate::vfs::udf::descriptor::{
Lvd, MetadataPartitionMap, PartitionDescriptor, PartitionMap,
};
use crate::vfs::udf::{Extent, LbAddr, LongAd};
const SS: usize = 2048;
fn put(buf: &mut [u8], off: usize, data: &[u8]) {
for (dst, &src) in buf.iter_mut().skip(off).zip(data) {
*dst = src;
}
}
fn fix_tag(buf: &mut [u8], off: usize) {
let mut sum: u8 = 0;
for (i, b) in buf.iter().skip(off).take(16).enumerate() {
if i != 4 {
sum = sum.wrapping_add(*b);
}
}
put(buf, off + 4, &[sum]);
}
fn sad(kind: u8, len: u32, block: u32) -> Vec<u8> {
let raw = u32::from(kind).wrapping_shl(30) | (len & 0x3FFF_FFFF);
let mut v = raw.to_le_bytes().to_vec();
v.extend_from_slice(&block.to_le_bytes());
v
}
fn lad(kind: u8, len: u32, block: u32, part: u16) -> Vec<u8> {
let raw = u32::from(kind).wrapping_shl(30) | (len & 0x3FFF_FFFF);
let mut v = raw.to_le_bytes().to_vec();
v.extend_from_slice(&block.to_le_bytes());
v.extend_from_slice(&part.to_le_bytes());
v.extend_from_slice(&[0_u8; 6]);
v
}
fn fe(id: u16, file_type: u8, flags: u16, info_len: u64, ad: &[u8]) -> Vec<u8> {
let mut buf = vec![0_u8; SS];
put(&mut buf, 0, &id.to_le_bytes());
put(&mut buf, 2, &3_u16.to_le_bytes());
put(&mut buf, 20, &4_u16.to_le_bytes()); put(&mut buf, 27, &[file_type]); put(&mut buf, 34, &flags.to_le_bytes()); put(&mut buf, 56, &info_len.to_le_bytes());
let (l_ea, l_ad, ad_base) = if id == 266 { (208, 212, 216) } else { (168, 172, 176) };
put(&mut buf, l_ea, &0_u32.to_le_bytes());
put(&mut buf, l_ad, &u32::try_from(ad.len()).unwrap_or(0).to_le_bytes());
put(&mut buf, ad_base, ad);
fix_tag(&mut buf, 0);
buf
}
fn fid(chars: u8, name: &str, block: u32, part: u16) -> Vec<u8> {
let mut cs0 = Vec::new();
if !name.is_empty() {
cs0.push(8_u8); cs0.extend_from_slice(name.as_bytes());
}
let raw = 38 + cs0.len();
let padded = (raw + 3) & !3_usize;
let mut buf = vec![0_u8; padded];
put(&mut buf, 0, &257_u16.to_le_bytes());
put(&mut buf, 2, &3_u16.to_le_bytes());
put(&mut buf, 18, &[chars]);
put(&mut buf, 19, &[u8::try_from(cs0.len()).unwrap_or(0)]);
put(&mut buf, 20, &0x800_u32.to_le_bytes()); put(&mut buf, 24, &block.to_le_bytes());
put(&mut buf, 28, &part.to_le_bytes());
put(&mut buf, 36, &0_u16.to_le_bytes()); put(&mut buf, 38, &cs0);
fix_tag(&mut buf, 0);
buf
}
fn aed(ads: &[u8]) -> Vec<u8> {
let mut buf = vec![0_u8; 24 + ads.len()];
put(&mut buf, 0, &258_u16.to_le_bytes());
put(&mut buf, 2, &3_u16.to_le_bytes());
put(&mut buf, 20, &u32::try_from(ads.len()).unwrap_or(0).to_le_bytes());
put(&mut buf, 24, ads);
fix_tag(&mut buf, 0);
buf
}
fn dir_data(fids: &[Vec<u8>]) -> Vec<u8> {
let mut out = Vec::new();
for f in fids {
out.extend_from_slice(f);
}
out
}
fn avdp(loc: u32, len: u32) -> Vec<u8> {
let mut buf = vec![0_u8; SS];
put(&mut buf, 0, &2_u16.to_le_bytes());
put(&mut buf, 2, &3_u16.to_le_bytes());
put(&mut buf, 16, &len.to_le_bytes());
put(&mut buf, 20, &loc.to_le_bytes());
fix_tag(&mut buf, 0);
buf
}
fn avdp_full(loc: u32, len: u32, res_loc: u32, res_len: u32) -> Vec<u8> {
let mut buf = avdp(loc, len);
put(&mut buf, 24, &res_len.to_le_bytes());
put(&mut buf, 28, &res_loc.to_le_bytes());
buf
}
fn vdp(loc: u32, len: u32) -> Vec<u8> {
let mut buf = vec![0_u8; SS];
put(&mut buf, 0, &3_u16.to_le_bytes());
put(&mut buf, 2, &3_u16.to_le_bytes());
put(&mut buf, 20, &len.to_le_bytes());
put(&mut buf, 24, &loc.to_le_bytes());
fix_tag(&mut buf, 0);
buf
}
fn pvd(label: &str) -> Vec<u8> {
let mut buf = vec![0_u8; SS];
put(&mut buf, 0, &1_u16.to_le_bytes());
put(&mut buf, 2, &3_u16.to_le_bytes());
put(&mut buf, 24, &[8]); put(&mut buf, 25, label.as_bytes());
put(&mut buf, 55, &[u8::try_from(1 + label.len()).unwrap_or(0)]);
fix_tag(&mut buf, 0);
buf
}
fn term() -> Vec<u8> {
let mut buf = vec![0_u8; SS];
put(&mut buf, 0, &8_u16.to_le_bytes());
put(&mut buf, 2, &3_u16.to_le_bytes());
fix_tag(&mut buf, 0);
buf
}
fn with_vdsn(mut desc: Vec<u8>, vdsn: u32) -> Vec<u8> {
put(&mut desc, 16, &vdsn.to_le_bytes());
desc
}
fn pd(number: u16, start: u32, length: u32) -> Vec<u8> {
let mut buf = vec![0_u8; SS];
put(&mut buf, 0, &5_u16.to_le_bytes());
put(&mut buf, 2, &3_u16.to_le_bytes());
put(&mut buf, 22, &number.to_le_bytes());
put(&mut buf, 188, &start.to_le_bytes());
put(&mut buf, 192, &length.to_le_bytes());
fix_tag(&mut buf, 0);
buf
}
fn phys_map(number: u16) -> Vec<u8> {
let mut v = vec![1_u8, 6, 0, 0];
v.extend_from_slice(&number.to_le_bytes());
v
}
fn meta_map(phys: u16, meta_loc: u32, mirror: u32) -> Vec<u8> {
let mut v = vec![0_u8; 64];
put(&mut v, 0, &[2, 64]);
put(&mut v, 5, b"*UDF Metadata Partition");
put(&mut v, 38, &phys.to_le_bytes());
put(&mut v, 40, &meta_loc.to_le_bytes());
put(&mut v, 44, &mirror.to_le_bytes());
v
}
fn lvd(
label: &str,
bs: u32,
fsd_block: u32,
fsd_part: u16,
maps: &[u8],
num_maps: u32,
) -> Vec<u8> {
lvd_with_fsd_len(label, bs, fsd_block, fsd_part, 0x800, maps, num_maps)
}
fn lvd_with_fsd_len(
label: &str,
bs: u32,
fsd_block: u32,
fsd_part: u16,
fsd_len: u32,
maps: &[u8],
num_maps: u32,
) -> Vec<u8> {
let mut buf = vec![0_u8; SS];
put(&mut buf, 0, &6_u16.to_le_bytes());
put(&mut buf, 2, &3_u16.to_le_bytes());
put(&mut buf, 84, &[8]);
put(&mut buf, 85, label.as_bytes());
put(&mut buf, 211, &[u8::try_from(1 + label.len()).unwrap_or(0)]); put(&mut buf, 212, &bs.to_le_bytes());
put(&mut buf, 248, &fsd_len.to_le_bytes());
put(&mut buf, 252, &fsd_block.to_le_bytes());
put(&mut buf, 256, &fsd_part.to_le_bytes());
put(&mut buf, 264, &u32::try_from(maps.len()).unwrap_or(0).to_le_bytes()); put(&mut buf, 268, &num_maps.to_le_bytes());
put(&mut buf, 440, maps);
fix_tag(&mut buf, 0);
buf
}
fn fsd(root_part: u16, root_block: u32) -> Vec<u8> {
let mut buf = vec![0_u8; SS];
put(&mut buf, 0, &0_u16.to_le_bytes()); put(&mut buf, 0, &256_u16.to_le_bytes());
put(&mut buf, 2, &3_u16.to_le_bytes());
put(&mut buf, 400, &0x800_u32.to_le_bytes());
put(&mut buf, 404, &root_block.to_le_bytes());
put(&mut buf, 408, &root_part.to_le_bytes());
fix_tag(&mut buf, 0);
buf
}
struct Iso {
bytes: Vec<u8>,
}
impl Iso {
fn new(sectors: usize) -> Self {
Self { bytes: vec![0_u8; sectors * SS] }
}
fn write(&mut self, sector: usize, data: &[u8]) {
put(&mut self.bytes, sector * SS, data);
}
fn into_bytes(self) -> Vec<u8> {
self.bytes
}
}
#[derive(Debug, Clone)]
struct MemIso {
data: Arc<[u8]>,
}
impl MemIso {
fn boxed(bytes: Vec<u8>) -> Box<dyn IsoReader> {
Box::new(Self { data: Arc::from(bytes) })
}
}
impl IsoReader for MemIso {
fn open(&self) -> std::io::Result<Box<dyn super::ReadSeek>> {
Ok(Box::new(std::io::Cursor::new(self.data.to_vec())))
}
}
fn raw_reader(
stream: Box<dyn super::ReadSeek>,
runs: Vec<Run>,
length: u64,
pos: u64,
) -> UdfFileReader {
UdfFileReader {
inner: Arc::new(UdfInner {
factory: MemIso::boxed(Vec::new()),
nodes: Vec::new(),
label: String::new(),
resilient: false,
errors: std::sync::Mutex::new(Vec::new()),
}),
name: "raw".to_owned(),
reported: false,
stream,
runs,
length,
pos,
}
}
fn read_all(file: &dyn BdFile) -> Vec<u8> {
let mut reader = file.open_read().expect("open_read");
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).expect("read_to_end");
bytes
}
fn file_names(dir: &dyn BdDir) -> Vec<String> {
let mut names: Vec<String> =
dir.get_files().expect("get_files").iter().map(|f| f.name().to_owned()).collect();
names.sort();
names
}
fn dir_names(dir: &dyn BdDir) -> Vec<String> {
let mut names: Vec<String> = dir
.get_directories()
.expect("get_directories")
.iter()
.map(|d| d.name().to_owned())
.collect();
names.sort();
names
}
fn physical_iso() -> Vec<u8> {
let mut iso = Iso::new(300);
iso.write(256, &avdp(257, 3 * SS as u32));
iso.write(257, &pd(0, 260, 50));
let mut maps = phys_map(0);
maps.extend_from_slice(&[9, 6, 0, 0, 0, 0]); iso.write(258, &lvd("VOLPHYS", SS as u32, 1, 0, &maps, 2));
iso.write(261, &fsd(0, 2));
let root_fids = dir_data(&[
fid(0x0A, "", 2, 0), fid(0x02, "SUB", 5, 0), fid(0x00, "data.bin", 6, 0), fid(0x00, "tiny.txt", 7, 0), fid(0x05, "gone", 9, 0), fid(0x01, "hide.x", 8, 0), fid(0x00, "dup", 6, 0), fid(0x00, "sparse.bin", 10, 0), ]);
let root_len = root_fids.len() as u64;
iso.write(262, &fe(261, 4, 0, root_len, &sad(0, root_len as u32, 3)));
iso.write(263, &root_fids);
let sub_fids = dir_data(&[fid(0x0A, "", 5, 0), fid(0x00, "leaf.txt", 11, 0)]);
iso.write(265, &fe(266, 4, 3, sub_fids.len() as u64, &sub_fids));
let mut data_ads = sad(0, SS as u32, 20);
data_ads.extend_from_slice(&sad(0, 100, 21));
iso.write(266, &fe(261, 5, 0, SS as u64 + 100, &data_ads));
iso.write(280, &vec![0xAB_u8; SS]); iso.write(281, &vec![0xCD_u8; SS]); iso.write(267, &fe(261, 5, 3, 5, b"hello"));
iso.write(268, &fe(261, 5, 0, 10, &sad(0, 10, 22)));
iso.write(282, b"0123456789");
iso.write(270, &fe(261, 5, 0, 16, &sad(1, 16, 99)));
iso.write(271, &fe(261, 5, 3, 4, b"leaf"));
iso.write(269, &fe(261, 5, 3, 4, b"gone"));
iso.into_bytes()
}
fn open_physical() -> UdfSource {
UdfSource::open(MemIso::boxed(physical_iso())).expect("open physical iso")
}
#[test]
fn physical_disc_volume_label_and_root_listing() {
let src = open_physical();
assert_eq!(src.volume_label(), "VOLPHYS");
let root = src.root();
assert_eq!(root.name(), "VOLPHYS");
assert_eq!(root.full_name(), "");
assert!(root.parent().is_none());
assert_eq!(
file_names(&root),
vec![
"data.bin".to_owned(),
"hide.x".to_owned(),
"sparse.bin".to_owned(),
"tiny.txt".to_owned(),
]
);
assert_eq!(dir_names(&root), vec!["SUB".to_owned()]);
}
#[test]
fn read_label_returns_the_volume_label_without_walking_the_tree() {
let factory = MemIso::boxed(physical_iso());
assert_eq!(UdfSource::read_label(&*factory).expect("read label"), "VOLPHYS");
}
#[test]
fn read_label_errors_on_a_non_udf_image() {
let factory = MemIso::boxed(vec![0_u8; 300 * SS]);
assert!(UdfSource::read_label(&*factory).is_err());
}
#[test]
fn read_label_propagates_an_open_failure() {
let factory = PathIso::new("no/such/bdinfo-rs-udf-xyzzy.iso");
assert!(UdfSource::read_label(&factory).is_err());
}
#[test]
fn physical_disc_reads_multi_extent_embedded_and_sparse_files() {
let src = open_physical();
let root = src.root();
let files = root.get_files().expect("files");
let by = |name: &str| files.iter().find(|f| f.name() == name).expect("file present");
let data = by("data.bin");
assert_eq!(data.length(), SS as u64 + 100);
assert_eq!(data.extension(), ".bin");
assert_eq!(data.full_name(), "/data.bin");
assert!(!data.is_dir());
let bytes = read_all(&**data);
assert_eq!(bytes.len(), SS + 100);
assert!(bytes.iter().take(SS).all(|&b| b == 0xAB));
assert!(bytes.iter().skip(SS).all(|&b| b == 0xCD));
let tiny = by("tiny.txt");
assert_eq!(tiny.length(), 5);
let mut text = String::new();
tiny.open_text().expect("open_text").read_to_string(&mut text).expect("read text");
assert_eq!(text, "hello");
assert_eq!(read_all(&**by("hide.x")), b"0123456789");
assert_eq!(read_all(&**by("sparse.bin")), vec![0_u8; 16]);
}
#[test]
fn physical_disc_nested_dir_parent_and_glob_recursion() {
let src = open_physical();
let root = src.root();
let dirs = root.get_directories().expect("dirs");
let sub = dirs.iter().find(|d| d.name() == "SUB").expect("SUB present");
assert_eq!(sub.full_name(), "/SUB");
assert_eq!(file_names(&**sub), vec!["leaf.txt".to_owned()]);
assert_eq!(sub.parent().expect("parent").name(), "VOLPHYS");
let top = root.get_files_pattern("*.txt").expect("glob top");
assert_eq!(top.iter().map(|f| f.name().to_owned()).collect::<Vec<_>>(), vec!["tiny.txt"]);
let mut deep: Vec<String> = root
.get_files_pattern_option("*.txt", SearchOption::AllDirectories)
.expect("glob deep")
.iter()
.map(|f| f.name().to_owned())
.collect();
deep.sort();
assert_eq!(deep, vec!["leaf.txt".to_owned(), "tiny.txt".to_owned()]);
}
fn embedded_file_iso(info_len: u64, inline: &[u8]) -> Vec<u8> {
let mut iso = Iso::new(264);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 50));
iso.write(258, &lvd("VOLEMB", SS as u32, 1, 0, &phys_map(0), 1));
iso.write(261, &fsd(0, 2));
let fids = dir_data(&[fid(0x0A, "", 2, 0), fid(0x00, "F", 3, 0)]);
iso.write(262, &fe(261, 4, 3, fids.len() as u64, &fids));
iso.write(263, &fe(261, 5, 3, info_len, inline));
iso.into_bytes()
}
#[test]
fn embedded_file_zero_extends_past_its_inline_bytes() {
let src = UdfSource::open(MemIso::boxed(embedded_file_iso(8, b"hi"))).expect("open");
let files = src.root().get_files().expect("files");
let f = files.iter().find(|f| f.name() == "F").expect("F present");
assert_eq!(f.length(), 8);
assert_eq!(read_all(&**f), b"hi\0\0\0\0\0\0");
}
#[test]
fn embedded_file_truncates_to_a_short_information_length() {
let src = UdfSource::open(MemIso::boxed(embedded_file_iso(3, b"abcde"))).expect("open");
let files = src.root().get_files().expect("files");
let f = files.iter().find(|f| f.name() == "F").expect("F present");
assert_eq!(f.length(), 3);
assert_eq!(read_all(&**f), b"abc");
}
#[test]
fn embedded_reader_serves_recorded_then_zero_fill_in_chunks() {
let mut reader = EmbeddedReader { bytes: b"ABC".to_vec(), length: 5, pos: 0 };
let mut buf = [0xFF_u8; 2];
assert_eq!(reader.read(&mut buf).expect("read 1"), 2);
assert_eq!(&buf, b"AB");
assert_eq!(reader.read(&mut buf).expect("read 2"), 2);
assert_eq!(&buf, b"C\0");
assert_eq!(reader.read(&mut buf).expect("read 3"), 1);
assert_eq!(buf[0], 0);
assert_eq!(reader.read(&mut buf).expect("read 4"), 0);
}
#[test]
fn embedded_reader_empty_buffer_reads_zero_without_advancing() {
let mut reader = EmbeddedReader { bytes: b"ABC".to_vec(), length: 3, pos: 0 };
assert_eq!(reader.read(&mut []).expect("empty read"), 0);
let mut buf = [0_u8; 3];
assert_eq!(reader.read(&mut buf).expect("read"), 3);
assert_eq!(&buf, b"ABC");
}
#[test]
fn embedded_reader_seek_covers_each_anchor_and_eof() {
let mut reader = EmbeddedReader { bytes: b"ABCD".to_vec(), length: 6, pos: 0 };
assert_eq!(reader.seek(SeekFrom::End(-1)).expect("seek end"), 5);
let mut buf = [0xFF_u8; 4];
assert_eq!(reader.read(&mut buf).expect("read tail"), 1);
assert_eq!(buf[0], 0);
assert_eq!(reader.seek(SeekFrom::Start(1)).expect("seek start"), 1);
assert_eq!(reader.seek(SeekFrom::Current(1)).expect("seek current"), 2);
assert_eq!(reader.read(&mut buf).expect("read mid"), 4);
assert_eq!(&buf, b"CD\0\0");
assert!(reader.seek(SeekFrom::Current(-100)).is_err());
}
fn metadata_iso() -> Vec<u8> {
let mut iso = Iso::new(330);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 300, 100));
let mut maps = phys_map(0);
maps.extend_from_slice(&meta_map(0, 2, 3)); iso.write(258, &lvd("VOLMETA", SS as u32, 1, 1, &maps, 2));
iso.write(302, &fe(261, 250, 0, 6 * SS as u64, &sad(0, 6 * SS as u32, 10)));
iso.write(311, &fsd(1, 2));
let root_fids = dir_data(&[fid(0x0A, "", 2, 1), fid(0x00, "FILE.BIN", 4, 1)]);
iso.write(312, &fe(261, 4, 0, root_fids.len() as u64, &sad(0, root_fids.len() as u32, 3)));
iso.write(313, &root_fids);
iso.write(314, &fe(261, 5, 1, 64, &lad(0, 64, 20, 0)));
iso.write(320, b"physical-partition-data-bytes-payload-0123456789ABCDEF0123456789");
iso.into_bytes()
}
#[test]
fn metadata_partition_disc_resolves_through_the_metadata_file() {
let src = UdfSource::open(MemIso::boxed(metadata_iso())).expect("open metadata iso");
assert_eq!(src.volume_label(), "VOLMETA");
let root = src.root();
assert_eq!(file_names(&root), vec!["FILE.BIN".to_owned()]);
let files = root.get_files().expect("files");
let file = files.first().expect("FILE.BIN");
assert_eq!(file.length(), 64);
assert_eq!(
read_all(&**file),
b"physical-partition-data-bytes-payload-0123456789ABCDEF0123456789"
);
}
fn mirrored_metadata_iso() -> Vec<u8> {
let mut iso = Iso::new(360);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 300, 100));
let mut maps = phys_map(0);
maps.extend_from_slice(&meta_map(0, 2, 3));
iso.write(258, &lvd("VOLMIR", SS as u32, 1, 1, &maps, 2));
iso.write(302, &fe(261, 250, 0, 6 * SS as u64, &sad(0, 6 * SS as u32, 10)));
iso.write(303, &fe(261, 251, 0, 6 * SS as u64, &sad(0, 6 * SS as u32, 40)));
let root_fids = dir_data(&[fid(0x0A, "", 2, 1), fid(0x00, "FILE.BIN", 4, 1)]);
for base in [310_usize, 340] {
iso.write(base + 1, &fsd(1, 2));
iso.write(
base + 2,
&fe(261, 4, 0, root_fids.len() as u64, &sad(0, root_fids.len() as u32, 3)),
);
iso.write(base + 3, &root_fids);
iso.write(base + 4, &fe(261, 5, 1, 64, &lad(0, 64, 20, 0)));
}
iso.write(320, b"physical-partition-data-bytes-payload-0123456789ABCDEF0123456789");
iso.into_bytes()
}
fn assert_opens_via_mirror(bytes: Vec<u8>, sector: u64) {
let bad = (sector * SS as u64)..((sector + 1) * SS as u64);
let src = UdfSource::open(FaultyIso::boxed(bytes, bad))
.expect("the mirror serves every injected bad metadata sector");
assert_eq!(src.volume_label(), "VOLMIR");
let root = src.root();
assert_eq!(file_names(&root), vec!["FILE.BIN".to_owned()]);
let files = root.get_files().expect("files");
assert_eq!(
read_all(&**files.first().expect("FILE.BIN")),
b"physical-partition-data-bytes-payload-0123456789ABCDEF0123456789"
);
}
#[test]
fn mirror_extent_runs_covers_each_arm() {
use super::mirror_extent_runs;
let locs = vec![
PartitionLoc::Physical { start: 100 },
PartitionLoc::Metadata {
phys_start: 200,
extents: Vec::new(),
mirror_extents: vec![Extent {
partition_ref: None,
block: 7,
length: 4 * SS as u32,
kind: ExtentKind::RecordedAllocated,
}],
},
];
let rec = |length: u32, kind| Extent { partition_ref: Some(1), block: 1, length, kind };
assert_eq!(
mirror_extent_runs(&locs, 0, &rec(SS as u32, ExtentKind::RecordedAllocated), SS as u64),
Some(vec![Run { src: Some(208 * SS as u64), len: SS as u64 }])
);
assert_eq!(
mirror_extent_runs(&locs, 0, &rec(0, ExtentKind::RecordedAllocated), SS as u64),
None
);
assert_eq!(
mirror_extent_runs(&locs, 0, &rec(16, ExtentKind::NotRecordedAllocated), SS as u64),
None
);
assert_eq!(
mirror_extent_runs(&locs, 0, &rec(16, ExtentKind::NotRecordedNotAllocated), SS as u64),
None
);
let phys = Extent {
partition_ref: Some(0),
block: 1,
length: 16,
kind: ExtentKind::RecordedAllocated,
};
assert_eq!(mirror_extent_runs(&locs, 0, &phys, SS as u64), None);
let oob = Extent {
partition_ref: Some(9),
block: 1,
length: 16,
kind: ExtentKind::RecordedAllocated,
};
assert_eq!(mirror_extent_runs(&locs, 0, &oob, SS as u64), None);
}
#[test]
fn mirrored_metadata_iso_opens_cleanly() {
let src = UdfSource::open(MemIso::boxed(mirrored_metadata_iso())).expect("open");
assert_eq!(src.volume_label(), "VOLMIR");
assert_eq!(file_names(&src.root()), vec!["FILE.BIN".to_owned()]);
}
#[test]
fn mirror_recovers_each_unreadable_metadata_sector() {
for sector in [311_u64, 312, 313, 314] {
assert_opens_via_mirror(mirrored_metadata_iso(), sector);
}
}
#[test]
fn mirror_fe_recovers_an_unreadable_primary_metadata_fe() {
assert_opens_via_mirror(mirrored_metadata_iso(), 302);
}
#[test]
fn mirror_fe_recovers_an_unparsable_primary_metadata_fe() {
let mut bytes = mirrored_metadata_iso();
put(&mut bytes, 302 * SS, &vec![0xFF_u8; SS]);
let src = UdfSource::open(MemIso::boxed(bytes)).expect("open via mirror fe");
assert_eq!(src.volume_label(), "VOLMIR");
assert_eq!(file_names(&src.root()), vec!["FILE.BIN".to_owned()]);
}
#[test]
fn unreadable_metadata_sector_without_a_mirror_fails() {
let bad = (313 * SS as u64)..(314 * SS as u64);
assert!(UdfSource::open(FaultyIso::boxed(metadata_iso(), bad)).is_err());
}
#[test]
fn identical_mirror_mapping_cannot_recover_and_fails() {
let mut bytes = mirrored_metadata_iso();
put(&mut bytes, 303 * SS, &fe(261, 251, 0, 6 * SS as u64, &sad(0, 6 * SS as u32, 10)));
let bad = (313 * SS as u64)..(314 * SS as u64);
assert!(UdfSource::open(FaultyIso::boxed(bytes, bad)).is_err());
}
fn extent_blocks(extents: &[Extent]) -> Vec<u32> {
extents.iter().map(|e| e.block).collect()
}
fn resolved_metadata_loc(primary: &[u8], mirror: &[u8]) -> (Vec<Extent>, Vec<Extent>) {
let lvd = Lvd {
logical_volume_identifier: String::new(),
logical_block_size: SS as u32,
file_set_descriptor: LongAd {
raw_length: 0,
location: LbAddr { block: 0, partition: 0 },
},
partition_maps: vec![
PartitionMap::Physical { partition_number: 0 },
PartitionMap::Metadata(MetadataPartitionMap {
physical_partition: 0,
metadata_file_location: 2,
metadata_mirror_file_location: 3,
}),
],
};
let pds =
vec![PartitionDescriptor { partition_number: 0, starting_location: 0, length: 100 }];
let mut image = vec![0_u8; 8 * SS];
put(&mut image, 2 * SS, primary);
put(&mut image, 3 * SS, mirror);
let mut cursor = std::io::Cursor::new(image);
let locs = resolve_partitions(&mut cursor, &lvd, &pds, SS as u64, Limits::DEFAULT)
.expect("resolve");
locs.into_iter()
.find_map(|loc| match loc {
PartitionLoc::Metadata { extents, mirror_extents, .. } => {
Some((extents, mirror_extents))
}
PartitionLoc::Physical { .. } => None,
})
.expect("a metadata partition")
}
#[test]
fn metadata_primary_with_wrong_file_type_falls_back_to_mirror() {
let primary = fe(261, 5, 0, SS as u64, &sad(0, SS as u32, 10));
let mirror = fe(261, 251, 0, SS as u64, &sad(0, SS as u32, 40));
let (extents, _) = resolved_metadata_loc(&primary, &mirror);
assert_eq!(extent_blocks(&extents), vec![40]);
}
#[test]
fn metadata_mirror_with_wrong_file_type_is_dropped() {
let primary = fe(261, 250, 0, SS as u64, &sad(0, SS as u32, 10));
let mirror = fe(261, 5, 0, SS as u64, &sad(0, SS as u32, 40));
let (extents, mirror_extents) = resolved_metadata_loc(&primary, &mirror);
assert_eq!(extent_blocks(&extents), vec![10]);
assert!(mirror_extents.is_empty(), "the wrong-type mirror is dropped");
}
#[test]
fn metadata_primary_without_extents_falls_back_to_mirror() {
let primary = fe(261, 250, 0, SS as u64, &[]);
let mirror = fe(261, 251, 0, SS as u64, &sad(0, SS as u32, 40));
let (extents, _) = resolved_metadata_loc(&primary, &mirror);
assert_eq!(extent_blocks(&extents), vec![40]);
}
#[test]
fn metadata_primary_embedded_falls_back_to_mirror() {
let primary = fe(261, 250, 3, 4, &[1, 2, 3, 4]);
let mirror = fe(261, 251, 0, SS as u64, &sad(0, SS as u32, 40));
let (extents, _) = resolved_metadata_loc(&primary, &mirror);
assert_eq!(extent_blocks(&extents), vec![40]);
}
#[test]
fn path_iso_opens_a_real_file_and_parses() {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
let mut path = std::env::temp_dir();
path.push(format!("bdinfo-rs-udf-{}-{unique}.iso", std::process::id()));
std::fs::File::create(&path)
.expect("create iso")
.write_all(&physical_iso())
.expect("write iso");
let factory = PathIso::new(&path);
assert!(factory.open().is_ok());
let src = UdfSource::open(Box::new(factory)).expect("open path iso");
assert_eq!(src.volume_label(), "VOLPHYS");
let _ = std::fs::remove_file(&path).is_ok();
}
#[test]
fn open_too_short_image_is_io_error() {
let err = UdfSource::open(MemIso::boxed(vec![0_u8; 16])).expect_err("too short");
assert!(err.to_string().starts_with("io error"));
}
#[test]
fn open_without_descriptors_is_structure_not_found() {
let err = UdfSource::open(MemIso::boxed(vec![0_u8; 258 * SS])).expect_err("no avdp");
assert_eq!(err.to_string(), "unable to locate BD structure");
}
#[test]
fn open_with_anchor_only_at_the_last_sector_succeeds() {
let mut bytes = physical_iso(); put(&mut bytes, 299 * SS, &avdp(257, 3 * SS as u32));
for slot in bytes.iter_mut().skip(256 * SS).take(SS) {
*slot = 0;
}
let src = UdfSource::open(MemIso::boxed(bytes)).expect("open via last-sector anchor");
assert_eq!(src.volume_label(), "VOLPHYS");
assert!(file_names(&src.root()).contains(&"data.bin".to_owned()));
}
#[test]
fn open_with_anchor_only_at_last_minus_256_succeeds() {
let mut bytes = physical_iso(); put(&mut bytes, 43 * SS, &avdp(257, 3 * SS as u32));
for slot in bytes.iter_mut().skip(256 * SS).take(SS) {
*slot = 0;
}
let src = UdfSource::open(MemIso::boxed(bytes)).expect("open via N-256 anchor");
assert_eq!(src.volume_label(), "VOLPHYS");
}
#[test]
fn locate_avdp_keeps_the_primary_error_when_the_length_is_unknown() {
let err = super::locate_avdp(&mut FailIo { fail_seek: true }).expect_err("no avdp");
assert!(err.to_string().starts_with("io error"));
}
fn write_minimal_partition_body(iso: &mut Iso) {
iso.write(261, &fsd(0, 2));
iso.write(262, &fe(261, 4, 3, 0, &[]));
}
fn vdp_iso() -> Vec<u8> {
let mut iso = Iso::new(300);
iso.write(256, &avdp(257, SS as u32));
iso.write(257, &vdp(280, 2 * SS as u32));
iso.write(280, &pd(0, 260, 30));
iso.write(281, &lvd("VOLVDP", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
iso.into_bytes()
}
#[test]
fn vds_continues_through_a_volume_descriptor_pointer() {
let src = UdfSource::open(MemIso::boxed(vdp_iso())).expect("open via vdp");
assert_eq!(src.volume_label(), "VOLVDP");
assert!(file_names(&src.root()).is_empty());
}
#[test]
fn self_referencing_vdp_chain_is_bounded() {
let mut iso = Iso::new(300);
iso.write(256, &avdp(257, SS as u32));
iso.write(257, &vdp(257, SS as u32));
let err = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect_err("no lvd");
assert_eq!(err.to_string(), "unable to locate BD structure");
}
#[test]
fn terminating_descriptor_stops_the_vds_walk() {
let mut iso = Iso::new(300);
iso.write(256, &avdp(257, 4 * SS as u32));
iso.write(257, &pd(0, 260, 50));
iso.write(258, &lvd("VOLTERM", SS as u32, 1, 0, &phys_map(0), 1));
iso.write(259, &term());
iso.write(260, &lvd("VOLDEAD", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert_eq!(src.volume_label(), "VOLTERM");
}
#[test]
fn reserve_vds_recovers_an_unusable_main_vds() {
let mut iso = Iso::new(300);
iso.write(256, &avdp_full(40, 2 * SS as u32, 257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 30));
iso.write(258, &lvd("VOLRES", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open via reserve");
assert_eq!(src.volume_label(), "VOLRES");
}
#[test]
fn reserve_vds_recovers_an_unreadable_main_vds() {
let mut iso = Iso::new(300);
iso.write(256, &avdp_full(9000, 2 * SS as u32, 257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 30));
iso.write(258, &lvd("VOLRES2", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open via reserve");
assert_eq!(src.volume_label(), "VOLRES2");
}
#[test]
fn reserve_vds_recovers_a_main_vds_with_no_partition_descriptor() {
let mut iso = Iso::new(300);
iso.write(256, &avdp_full(257, SS as u32, 270, 2 * SS as u32));
iso.write(257, &lvd("VOLMAIN", SS as u32, 1, 0, &phys_map(0), 1));
iso.write(270, &pd(0, 260, 30));
iso.write(271, &lvd("VOLRES3", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open via reserve");
assert_eq!(src.volume_label(), "VOLRES3");
}
#[test]
fn a_usable_main_vds_is_preferred_over_the_reserve() {
let mut iso = Iso::new(300);
iso.write(256, &avdp_full(257, 2 * SS as u32, 270, 2 * SS as u32));
iso.write(257, &pd(0, 260, 30));
iso.write(258, &lvd("VOLMAIN", SS as u32, 1, 0, &phys_map(0), 1));
iso.write(270, &pd(0, 260, 30));
iso.write(271, &lvd("VOLSPARE", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert_eq!(src.volume_label(), "VOLMAIN");
}
#[test]
fn unreadable_main_with_a_blank_reserve_propagates_the_main_error() {
let mut iso = Iso::new(300);
iso.write(256, &avdp_full(9000, 2 * SS as u32, 40, 2 * SS as u32));
let err = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect_err("main io error");
assert!(err.to_string().starts_with("io error"));
}
#[test]
fn unreadable_main_and_reserve_vds_propagates_the_main_failure() {
let mut iso = Iso::new(300);
iso.write(256, &avdp_full(9000, 2 * SS as u32, 9500, 2 * SS as u32));
let err = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect_err("both vds dead");
assert!(err.to_string().starts_with("io error"));
}
#[test]
fn highest_sequence_number_lvd_prevails() {
for (first, second) in [(("VOLHI", 2), ("VOLLO", 1)), (("VOLLO", 1), ("VOLHI", 2))] {
let mut iso = Iso::new(300);
iso.write(256, &avdp(257, 3 * SS as u32));
iso.write(257, &pd(0, 260, 50));
let mk = |(label, n): (&str, u32)| {
with_vdsn(lvd(label, SS as u32, 1, 0, &phys_map(0), 1), n)
};
iso.write(258, &mk(first));
iso.write(259, &mk(second));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert_eq!(src.volume_label(), "VOLHI");
}
}
#[test]
fn highest_sequence_number_partition_descriptor_prevails() {
for (first, second) in [((260, 2), (290, 1)), ((290, 1), (260, 2))] {
let mut iso = Iso::new(300);
iso.write(256, &avdp(257, 3 * SS as u32));
let mk = |(start, n): (u32, u32)| with_vdsn(pd(0, start, 50), n);
iso.write(257, &mk(first));
iso.write(258, &mk(second));
iso.write(259, &lvd("VOLPD", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert_eq!(src.volume_label(), "VOLPD");
}
}
#[test]
fn empty_lvd_identifier_falls_back_to_the_pvd_label() {
let mut iso = Iso::new(300);
iso.write(256, &avdp(257, 3 * SS as u32));
iso.write(257, &pd(0, 260, 50));
iso.write(258, &pvd("PVDLBL"));
iso.write(259, &lvd("", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert_eq!(src.volume_label(), "PVDLBL");
assert_eq!(src.root().name(), "PVDLBL");
}
#[test]
fn non_empty_lvd_identifier_wins_over_the_pvd() {
let mut iso = Iso::new(300);
iso.write(256, &avdp(257, 3 * SS as u32));
iso.write(257, &pd(0, 260, 50));
iso.write(258, &pvd("PVDLBL"));
iso.write(259, &lvd("LVDLBL", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert_eq!(src.volume_label(), "LVDLBL");
}
#[test]
fn empty_lvd_identifier_without_a_pvd_is_an_empty_label() {
let mut iso = Iso::new(300);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 50));
iso.write(258, &lvd("", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert_eq!(src.volume_label(), "");
}
#[test]
fn highest_sequence_number_pvd_prevails() {
for (first, second) in [(("PVDHI", 2), ("PVDLO", 1)), (("PVDLO", 1), ("PVDHI", 2))] {
let mut iso = Iso::new(300);
iso.write(256, &avdp(257, 4 * SS as u32));
iso.write(257, &pd(0, 260, 50));
let mk = |(label, n): (&str, u32)| with_vdsn(pvd(label), n);
iso.write(258, &mk(first));
iso.write(259, &mk(second));
iso.write(260, &lvd("", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert_eq!(src.volume_label(), "PVDHI");
}
}
#[test]
fn equal_sequence_number_pvds_keep_the_later_one() {
let mut iso = Iso::new(300);
iso.write(256, &avdp(257, 4 * SS as u32));
iso.write(257, &pd(0, 260, 50));
iso.write(258, &pvd("PVDOLD"));
iso.write(259, &pvd("PVDNEW"));
iso.write(260, &lvd("", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert_eq!(src.volume_label(), "PVDNEW");
}
#[test]
fn equal_sequence_numbers_keep_the_later_descriptor() {
let mut iso = Iso::new(300);
iso.write(256, &avdp(257, 5 * SS as u32));
iso.write(257, &pd(0, 290, 50)); iso.write(258, &pd(0, 260, 50)); iso.write(259, &lvd("VOLOLD", SS as u32, 1, 0, &phys_map(0), 1));
iso.write(260, &lvd("VOLNEW", SS as u32, 1, 0, &phys_map(0), 1));
write_minimal_partition_body(&mut iso);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert_eq!(src.volume_label(), "VOLNEW");
}
#[test]
fn open_without_lvd_is_structure_not_found() {
let mut iso = Iso::new(260);
iso.write(256, &avdp(257, SS as u32));
iso.write(257, &pd(0, 1, 1)); let err = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect_err("no lvd");
assert_eq!(err.to_string(), "unable to locate BD structure");
}
#[test]
fn open_with_zero_block_size_is_structure_not_found() {
let mut iso = Iso::new(260);
iso.write(256, &avdp(257, SS as u32));
iso.write(257, &lvd("X", 0, 1, 0, &phys_map(0), 1));
let err = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect_err("bs 0");
assert_eq!(err.to_string(), "unable to locate BD structure");
}
#[test]
fn open_with_unmatched_partition_is_structure_not_found() {
let mut iso = Iso::new(265);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 10));
iso.write(258, &lvd("X", SS as u32, 1, 0, &phys_map(7), 1));
let err = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect_err("no match");
assert_eq!(err.to_string(), "unable to locate BD structure");
}
#[test]
fn open_with_bad_fsd_is_structure_not_found() {
let mut iso = Iso::new(265);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 10));
iso.write(258, &lvd("X", SS as u32, 1, 0, &phys_map(0), 1));
let err = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect_err("bad fsd");
assert_eq!(err.to_string(), "unable to locate BD structure");
}
#[test]
fn open_with_bad_metadata_file_entry_is_structure_not_found() {
let mut iso = Iso::new(330);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 300, 100));
let mut maps = phys_map(0);
maps.extend_from_slice(&meta_map(0, 2, 3));
iso.write(258, &lvd("X", SS as u32, 1, 1, &maps, 2));
let err = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect_err("bad meta fe");
assert_eq!(err.to_string(), "unable to locate BD structure");
}
#[test]
fn open_with_unresolvable_fsd_partition_is_structure_not_found() {
let mut iso = Iso::new(265);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 10));
iso.write(258, &lvd("X", SS as u32, 1, 5, &phys_map(0), 1));
let err = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect_err("bad fsd ref");
assert_eq!(err.to_string(), "unable to locate BD structure");
}
fn fsd_search_iso(fsd_len: u32) -> Iso {
let mut iso = Iso::new(280);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 50));
iso.write(258, &lvd_with_fsd_len("VOLFSD", SS as u32, 1, 0, fsd_len, &phys_map(0), 1));
let fids = dir_data(&[fid(0x0A, "", 5, 0), fid(0x00, "FILE.BIN", 6, 0)]);
iso.write(265, &fe(266, 4, 3, fids.len() as u64, &fids));
iso.write(266, &fe(261, 5, 3, 5, b"hello"));
iso
}
#[test]
fn fsd_search_finds_the_descriptor_past_the_first_block() {
let mut iso = fsd_search_iso(8 * SS as u32);
iso.write(263, &fsd(0, 5));
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open via FSD search");
assert_eq!(src.volume_label(), "VOLFSD");
assert_eq!(file_names(&src.root()), vec!["FILE.BIN".to_owned()]);
}
#[test]
fn fsd_search_reads_the_first_block_with_a_zero_length_extent() {
let mut iso = fsd_search_iso(0);
iso.write(261, &fsd(0, 5)); let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open via fast path");
assert_eq!(file_names(&src.root()), vec!["FILE.BIN".to_owned()]);
}
#[test]
fn fsd_search_stops_at_a_terminating_descriptor() {
let mut iso = fsd_search_iso(8 * SS as u32);
iso.write(261, &term());
iso.write(263, &fsd(0, 5));
let err = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect_err("terminated search");
assert_eq!(err.to_string(), "unable to locate BD structure");
}
#[test]
fn fsd_search_stops_at_the_cap() {
let mut iso = fsd_search_iso(8 * SS as u32);
iso.write(263, &fsd(0, 5));
let bytes = iso.into_bytes();
let mut cursor = std::io::Cursor::new(bytes.clone());
let capped = Limits { max_fsd_sectors: 2, ..Limits::DEFAULT };
assert!(parse_volume(&mut cursor, capped).is_err());
let mut cursor = std::io::Cursor::new(bytes);
let reaching = Limits { max_fsd_sectors: 3, ..Limits::DEFAULT };
let volume = parse_volume(&mut cursor, reaching).expect("fsd found within the cap");
assert_eq!(volume.root_icb, (0, 5));
}
#[test]
fn max_nodes_cap_stops_the_walk() {
let bytes = physical_iso();
let mut cursor = std::io::Cursor::new(bytes);
let volume = parse_volume(&mut cursor, Limits::DEFAULT).expect("volume");
let tiny = Limits { max_nodes: 1, ..Limits::DEFAULT };
let nodes = build_tree(&mut cursor, &volume, tiny).expect("tree");
assert_eq!(nodes.len(), 1); }
#[test]
fn max_dir_bytes_cap_yields_no_children() {
let bytes = physical_iso();
let mut cursor = std::io::Cursor::new(bytes);
let volume = parse_volume(&mut cursor, Limits::DEFAULT).expect("volume");
let no_dir = Limits { max_dir_bytes: 0, ..Limits::DEFAULT };
let nodes = build_tree(&mut cursor, &volume, no_dir).expect("tree");
assert_eq!(nodes.len(), 1);
}
fn deep_chain_iso(depth: usize) -> Vec<u8> {
let mut iso = Iso::new(270 + depth);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, (depth + 16) as u32));
iso.write(258, &lvd("DEEP", SS as u32, 1, 0, &phys_map(0), 1));
iso.write(261, &fsd(0, 2)); for k in 0..=depth {
let own_block = 2 + k;
let child_block = own_block + 1;
let child = if k < depth {
fid(0x02, "D", child_block as u32, 0) } else {
fid(0x00, "leaf.bin", child_block as u32, 0) };
let fids = dir_data(&[fid(0x0A, "", own_block as u32, 0), child]);
iso.write(260 + own_block, &fe(266, 4, 3, fids.len() as u64, &fids));
}
iso.write(263 + depth, &fe(261, 5, 3, 1, b"x"));
iso.into_bytes()
}
fn dir_node_count(nodes: &[Node]) -> usize {
nodes.iter().filter(|n| matches!(n.kind, NodeKind::Dir)).count()
}
#[test]
fn max_depth_cap_keeps_a_chain_exactly_at_the_cap() {
const DEPTH: usize = 4;
let bytes = deep_chain_iso(DEPTH);
let mut cursor = std::io::Cursor::new(bytes);
let volume = parse_volume(&mut cursor, Limits::DEFAULT).expect("volume");
let at_cap = Limits { max_depth: DEPTH, ..Limits::DEFAULT };
let nodes = build_tree(&mut cursor, &volume, at_cap).expect("tree");
assert_eq!(dir_node_count(&nodes), DEPTH + 1); let deepest = "/D".repeat(DEPTH);
assert!(
nodes.iter().any(|n| n.full_name == deepest),
"the directory at depth == max_depth must be present"
);
assert!(
nodes.iter().any(|n| n.full_name == format!("{deepest}/leaf.bin")),
"the leaf file under the deepest expanded directory must be kept"
);
}
#[test]
fn max_depth_cap_drops_subdirectories_past_the_cap() {
const DEPTH: usize = 5; let cap = DEPTH - 1; let bytes = deep_chain_iso(DEPTH);
let mut cursor = std::io::Cursor::new(bytes);
let volume = parse_volume(&mut cursor, Limits::DEFAULT).expect("volume");
let limited = Limits { max_depth: cap, ..Limits::DEFAULT };
let nodes = build_tree(&mut cursor, &volume, limited).expect("tree");
assert_eq!(dir_node_count(&nodes), cap + 1); let dropped = "/D".repeat(cap + 1); assert!(
!nodes.iter().any(|n| n.full_name == dropped),
"the subdirectory just past the cap must be truncated"
);
assert!(!nodes.iter().any(|n| n.full_name.ends_with("/leaf.bin")));
}
#[test]
fn a_deep_hostile_chain_yields_a_bounded_arena_without_panicking() {
const DEPTH: usize = 64;
let cap = 3;
let bytes = deep_chain_iso(DEPTH);
let mut cursor = std::io::Cursor::new(bytes);
let volume = parse_volume(&mut cursor, Limits::DEFAULT).expect("volume");
let limited = Limits { max_depth: cap, ..Limits::DEFAULT };
let nodes = build_tree(&mut cursor, &volume, limited).expect("tree");
assert_eq!(dir_node_count(&nodes), cap + 1);
assert_eq!(nodes.len(), cap + 1);
}
#[test]
fn deep_chain_recursive_glob_walks_the_whole_tree_under_the_default_cap() {
let src = UdfSource::open(MemIso::boxed(deep_chain_iso(6))).expect("open chain");
let hits = src
.root()
.get_files_pattern_option("leaf.bin", SearchOption::AllDirectories)
.expect("recursive glob");
assert_eq!(hits.iter().map(|f| f.name().to_owned()).collect::<Vec<_>>(), vec!["leaf.bin"]);
}
#[test]
fn directory_with_unreadable_child_skips_it() {
let mut iso = Iso::new(280);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 50));
iso.write(258, &lvd("X", SS as u32, 1, 0, &phys_map(0), 1));
iso.write(261, &fsd(0, 2));
let fids = dir_data(&[
fid(0x0A, "", 2, 0),
fid(0x00, "ok.txt", 4, 0),
fid(0x00, "bad.txt", 9000, 0), fid(0x00, "off.txt", 0, 9), ]);
iso.write(262, &fe(261, 4, 0, fids.len() as u64, &sad(0, fids.len() as u32, 3)));
iso.write(263, &fids);
iso.write(264, &fe(261, 5, 3, 2, b"ok")); let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert_eq!(file_names(&src.root()), vec!["ok.txt".to_owned()]);
}
#[test]
fn child_with_a_non_direct_icb_strategy_is_skipped() {
let mut iso = Iso::new(280);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 50));
iso.write(258, &lvd("X", SS as u32, 1, 0, &phys_map(0), 1));
iso.write(261, &fsd(0, 2));
let fids = dir_data(&[
fid(0x0A, "", 2, 0),
fid(0x00, "ok.txt", 4, 0),
fid(0x00, "chained.txt", 5, 0), ]);
iso.write(262, &fe(261, 4, 0, fids.len() as u64, &sad(0, fids.len() as u32, 3)));
iso.write(263, &fids);
iso.write(264, &fe(261, 5, 3, 2, b"ok"));
let mut chained = fe(261, 5, 3, 2, b"no");
put(&mut chained, 20, &4096_u16.to_le_bytes()); iso.write(265, &chained);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert_eq!(file_names(&src.root()), vec!["ok.txt".to_owned()]);
}
#[test]
fn root_with_a_non_direct_icb_strategy_opens_empty() {
let mut iso = Iso::new(280);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 50));
iso.write(258, &lvd("X", SS as u32, 1, 0, &phys_map(0), 1));
iso.write(261, &fsd(0, 2));
let mut root = fe(261, 4, 3, 0, &[]);
put(&mut root, 20, &4096_u16.to_le_bytes());
iso.write(262, &root);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert!(file_names(&src.root()).is_empty());
assert!(dir_names(&src.root()).is_empty());
}
#[test]
fn child_with_unparsable_entry_is_skipped() {
let mut iso = Iso::new(280);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 50));
iso.write(258, &lvd("X", SS as u32, 1, 0, &phys_map(0), 1));
iso.write(261, &fsd(0, 2));
let fids = dir_data(&[fid(0x0A, "", 2, 0), fid(0x00, "junk", 4, 0)]);
iso.write(262, &fe(261, 4, 0, fids.len() as u64, &sad(0, fids.len() as u32, 3)));
iso.write(263, &fids);
iso.write(264, &vec![0xFF_u8; SS]);
let src = UdfSource::open(MemIso::boxed(iso.into_bytes())).expect("open");
assert!(file_names(&src.root()).is_empty());
}
#[test]
fn udf_dir_on_a_non_directory_node_errors() {
let src = open_physical();
let bogus = UdfDir {
inner: Arc::clone(&src.inner),
node: 999_999,
name: String::new(),
full_name: String::new(),
parent: None,
};
assert!(bogus.get_files().is_err());
assert!(bogus.get_directories().is_err());
}
#[test]
fn extension_of_handles_dotted_and_bare_names() {
assert_eq!(extension_of("00000.MPLS"), ".MPLS");
assert_eq!(extension_of("a.b.ssif"), ".ssif");
assert_eq!(extension_of("README"), "");
assert_eq!(extension_of("trailing."), ".");
}
#[test]
fn offset_by_clamps_and_overflows() {
assert_eq!(offset_by(10, 5), Some(15));
assert_eq!(offset_by(10, -4), Some(6));
assert_eq!(offset_by(3, -4), None); assert_eq!(offset_by(u64::MAX, 1), None); assert_eq!(offset_by(10, i64::MIN), None); }
#[test]
fn metadata_sector_walks_extents() {
let extents = vec![
Extent {
partition_ref: None,
block: 10,
length: 2 * SS as u32,
kind: ExtentKind::RecordedAllocated,
},
Extent {
partition_ref: None,
block: 50,
length: 2 * SS as u32,
kind: ExtentKind::RecordedAllocated,
},
];
assert_eq!(metadata_sector(100, &extents, 0, SS as u64), Some(110));
assert_eq!(metadata_sector(100, &extents, 1, SS as u64), Some(111));
assert_eq!(metadata_sector(100, &extents, 2, SS as u64), Some(150));
assert_eq!(metadata_sector(100, &extents, 3, SS as u64), Some(151));
assert_eq!(metadata_sector(100, &extents, 4, SS as u64), None);
}
#[test]
fn metadata_runs_coalesces_and_splits() {
let contiguous = vec![Extent {
partition_ref: None,
block: 10,
length: 4 * SS as u32,
kind: ExtentKind::RecordedAllocated,
}];
let runs = metadata_runs(100, &contiguous, 0, 2 * SS as u64, SS as u64).expect("runs");
assert_eq!(runs, vec![Run { src: Some(110 * SS as u64), len: 2 * SS as u64 }]);
let split = vec![
Extent {
partition_ref: None,
block: 10,
length: SS as u32,
kind: ExtentKind::RecordedAllocated,
},
Extent {
partition_ref: None,
block: 80,
length: SS as u32,
kind: ExtentKind::RecordedAllocated,
},
];
let runs = metadata_runs(100, &split, 0, SS as u64 + 50, SS as u64).expect("runs");
assert_eq!(
runs,
vec![
Run { src: Some(110 * SS as u64), len: SS as u64 },
Run { src: Some(180 * SS as u64), len: 50 },
]
);
assert_eq!(metadata_runs(100, &contiguous, 0, 100 * SS as u64, SS as u64), None);
}
#[test]
fn extent_runs_covers_each_kind() {
let phys = vec![PartitionLoc::Physical { start: 100 }];
let rec = Extent {
partition_ref: None,
block: 5,
length: 2048,
kind: ExtentKind::RecordedAllocated,
};
assert_eq!(
extent_runs(&phys, 0, &rec, SS as u64),
Some(vec![Run { src: Some(105 * SS as u64), len: 2048 }])
);
let zero = Extent {
partition_ref: None,
block: 5,
length: 0,
kind: ExtentKind::RecordedAllocated,
};
assert_eq!(extent_runs(&phys, 0, &zero, SS as u64), Some(Vec::new()));
let sparse = Extent {
partition_ref: None,
block: 5,
length: 32,
kind: ExtentKind::NotRecordedAllocated,
};
assert_eq!(
extent_runs(&phys, 0, &sparse, SS as u64),
Some(vec![Run { src: None, len: 32 }])
);
let unalloc = Extent {
partition_ref: None,
block: 5,
length: 8,
kind: ExtentKind::NotRecordedNotAllocated,
};
assert_eq!(
extent_runs(&phys, 0, &unalloc, SS as u64),
Some(vec![Run { src: None, len: 8 }])
);
let next =
Extent { partition_ref: None, block: 5, length: 16, kind: ExtentKind::NextExtent };
assert_eq!(
extent_runs(&phys, 0, &next, SS as u64),
Some(vec![Run { src: Some(105 * SS as u64), len: 16 }])
);
let oob = Extent {
partition_ref: Some(9),
block: 1,
length: 16,
kind: ExtentKind::RecordedAllocated,
};
assert_eq!(extent_runs(&phys, 0, &oob, SS as u64), None);
let meta = vec![PartitionLoc::Metadata {
phys_start: 200,
extents: vec![Extent {
partition_ref: None,
block: 0,
length: 4 * SS as u32,
kind: ExtentKind::RecordedAllocated,
}],
mirror_extents: Vec::new(),
}];
let mrec = Extent {
partition_ref: None,
block: 1,
length: 2048,
kind: ExtentKind::RecordedAllocated,
};
assert_eq!(
extent_runs(&meta, 0, &mrec, SS as u64),
Some(vec![Run { src: Some(201 * SS as u64), len: 2048 }])
);
}
#[test]
fn resolve_sector_covers_physical_metadata_and_oob() {
let locs = vec![
PartitionLoc::Physical { start: 100 },
PartitionLoc::Metadata {
phys_start: 200,
extents: vec![Extent {
partition_ref: None,
block: 0,
length: 4 * SS as u32,
kind: ExtentKind::RecordedAllocated,
}],
mirror_extents: Vec::new(),
},
];
assert_eq!(resolve_sector(&locs, 0, 5, SS as u64), Some(105));
assert_eq!(resolve_sector(&locs, 1, 2, SS as u64), Some(202));
assert_eq!(resolve_sector(&locs, 9, 0, SS as u64), None);
assert_eq!(super::resolve_mirror_sector(&locs, 0, 5, SS as u64), None);
assert_eq!(super::resolve_mirror_sector(&locs, 1, 2, SS as u64), None);
assert_eq!(super::resolve_mirror_sector(&locs, 9, 0, SS as u64), None);
}
#[test]
fn read_runs_reads_data_zeros_and_caps() {
let mut cursor = std::io::Cursor::new(b"ABCDEFGH".to_vec());
let runs = vec![
Run { src: Some(0), len: 4 },
Run { src: None, len: 2 },
Run { src: Some(4), len: 4 },
];
let out = read_runs(&mut cursor, &runs, 8).expect("read");
assert_eq!(out, b"ABCD\0\0EF");
assert!(read_runs(&mut cursor, &runs, 0).expect("read").is_empty());
}
#[test]
fn udf_file_reader_reads_across_runs_and_seeks() {
let cursor = std::io::Cursor::new(b"ABCDEFGHIJ".to_vec());
let mut reader = raw_reader(
Box::new(cursor),
vec![
Run { src: Some(0), len: 3 }, Run { src: None, len: 2 }, Run { src: Some(5), len: 3 }, ],
8,
0,
);
let mut all = Vec::new();
reader.read_to_end(&mut all).expect("read");
assert_eq!(all, b"ABC\0\0FGH");
assert_eq!(reader.seek(SeekFrom::Start(1)).expect("start"), 1);
let mut one = [0_u8; 2];
reader.read_exact(&mut one).expect("read");
assert_eq!(&one, b"BC");
assert_eq!(reader.seek(SeekFrom::End(-3)).expect("end"), 5);
assert_eq!(reader.seek(SeekFrom::Current(-2)).expect("cur"), 3);
assert_eq!(reader.seek(SeekFrom::Start(100)).expect("far"), 100);
let mut none = [0_u8; 4];
assert_eq!(reader.read(&mut none).expect("eof"), 0);
assert!(reader.seek(SeekFrom::Start(0)).is_ok());
assert!(reader.seek(SeekFrom::Current(-1)).is_err());
assert_eq!(reader.read(&mut []).expect("empty"), 0);
}
#[test]
fn udf_file_reader_truncated_runs_report_eof() {
let cursor = std::io::Cursor::new(b"AB".to_vec());
let mut reader = raw_reader(Box::new(cursor), vec![Run { src: Some(0), len: 2 }], 10, 5);
let mut buf = [0_u8; 4];
assert_eq!(reader.read(&mut buf).expect("eof"), 0);
}
#[test]
fn collect_extents_follows_a_next_extent_continuation() {
let mut ads = sad(0, 4096, 77); ads.extend_from_slice(&sad(0, 0, 0)); let cont = aed(&ads);
let mut image = vec![0_u8; 4 * SS];
put(&mut image, 2 * SS, &cont); let mut cursor = std::io::Cursor::new(image);
let mut ad = sad(0, 2048, 40); ad.extend_from_slice(&sad(3, SS as u32, 2)); let entry_buf = fe(261, 5, 0, 6144, &ad);
let entry = FileEntry::parse(&entry_buf).expect("fe");
let locs = vec![PartitionLoc::Physical { start: 0 }];
let extents = collect_extents(&mut cursor, &locs, &entry, 0, SS as u64, Limits::DEFAULT)
.expect("extents");
assert_eq!(extents.len(), 2);
assert_eq!(extents.first().map(|e| e.block), Some(40));
assert_eq!(extents.get(1).map(|e| e.block), Some(77));
}
#[test]
fn collect_extents_skips_a_continuation_without_an_aed_header() {
let mut cont = sad(0, 4096, 77);
cont.extend_from_slice(&sad(0, 0, 0));
let mut image = vec![0_u8; 4 * SS];
put(&mut image, 2 * SS, &cont); let mut cursor = std::io::Cursor::new(image);
let mut ad = sad(3, SS as u32, 2); ad.extend_from_slice(&sad(0, 2048, 40)); let entry = FileEntry::parse(&fe(261, 5, 0, 2048, &ad)).expect("fe");
let locs = vec![PartitionLoc::Physical { start: 0 }];
let extents = collect_extents(&mut cursor, &locs, &entry, 0, SS as u64, Limits::DEFAULT)
.expect("extents");
assert_eq!(extents.iter().map(|e| e.block).collect::<Vec<_>>(), vec![40]);
}
#[test]
fn collect_extents_skips_a_continuation_with_a_wrong_tag_id() {
let mut not_aed = aed(&sad(0, 4096, 77));
put(&mut not_aed, 0, &261_u16.to_le_bytes());
fix_tag(&mut not_aed, 0);
let mut image = vec![0_u8; 4 * SS];
put(&mut image, 2 * SS, ¬_aed);
let mut cursor = std::io::Cursor::new(image);
let entry = FileEntry::parse(&fe(261, 5, 0, 2048, &sad(3, SS as u32, 2))).expect("fe");
let locs = vec![PartitionLoc::Physical { start: 0 }];
let extents = collect_extents(&mut cursor, &locs, &entry, 0, SS as u64, Limits::DEFAULT)
.expect("extents");
assert!(extents.is_empty());
}
#[test]
fn collect_extents_honors_the_aed_l_ad_bound() {
let mut cont = aed(&sad(0, 4096, 77));
put(&mut cont, 32, &sad(0, 2048, 99)); let mut image = vec![0_u8; 4 * SS];
put(&mut image, 2 * SS, &cont);
let mut cursor = std::io::Cursor::new(image);
let entry = FileEntry::parse(&fe(261, 5, 0, 4096, &sad(3, SS as u32, 2))).expect("fe");
let locs = vec![PartitionLoc::Physical { start: 0 }];
let extents = collect_extents(&mut cursor, &locs, &entry, 0, SS as u64, Limits::DEFAULT)
.expect("extents");
assert_eq!(extents.iter().map(|e| e.block).collect::<Vec<_>>(), vec![77]);
}
#[test]
fn aed_allocation_area_clamps_and_rejects_short_blocks() {
use super::aed_allocation_area;
let ads = sad(0, 4096, 77);
assert_eq!(aed_allocation_area(&aed(&ads)), Some(ads.as_slice()));
let mut overlong = aed(&ads);
put(&mut overlong, 20, &u32::MAX.to_le_bytes());
fix_tag(&mut overlong, 0);
assert_eq!(aed_allocation_area(&overlong), Some(ads.as_slice()));
let header_only = aed(&[]);
assert_eq!(aed_allocation_area(header_only.get(..18).unwrap_or_default()), None);
assert_eq!(aed_allocation_area(header_only.get(..8).unwrap_or_default()), None);
}
#[test]
fn collect_extents_bounds_a_self_referential_continuation() {
let mut image = vec![0_u8; 4 * SS];
put(&mut image, 2 * SS, &aed(&sad(3, SS as u32, 2))); let mut cursor = std::io::Cursor::new(image);
let ad = sad(3, SS as u32, 2); let entry_buf = fe(261, 5, 0, 0, &ad);
let entry = FileEntry::parse(&entry_buf).expect("fe");
let locs = vec![PartitionLoc::Physical { start: 0 }];
let bounded = Limits { max_continuations: 3, ..Limits::DEFAULT };
let extents =
collect_extents(&mut cursor, &locs, &entry, 0, SS as u64, bounded).expect("extents");
assert!(extents.is_empty()); }
#[test]
fn collect_extents_follows_exactly_max_continuations() {
let mut image = vec![0_u8; 8 * SS];
let mut block_a = sad(0, 2048, 50); block_a.extend_from_slice(&sad(3, SS as u32, 3)); put(&mut image, 2 * SS, &aed(&block_a));
let mut block_b = sad(0, 2048, 51); block_b.extend_from_slice(&sad(3, SS as u32, 4)); put(&mut image, 3 * SS, &aed(&block_b));
let mut block_c = sad(0, 2048, 52); block_c.extend_from_slice(&sad(0, 0, 0)); put(&mut image, 4 * SS, &aed(&block_c));
let mut cursor = std::io::Cursor::new(image);
let mut ad = sad(0, 2048, 40); ad.extend_from_slice(&sad(3, SS as u32, 2)); let entry = FileEntry::parse(&fe(261, 5, 0, 9000, &ad)).expect("fe");
let locs = vec![PartitionLoc::Physical { start: 0 }];
let two = Limits { max_continuations: 2, ..Limits::DEFAULT };
let extents =
collect_extents(&mut cursor, &locs, &entry, 0, SS as u64, two).expect("extents");
assert_eq!(extents.iter().map(|e| e.block).collect::<Vec<_>>(), vec![40, 50, 51]);
}
fn flat_volume() -> Volume {
Volume {
block_size: SS as u64,
locs: vec![PartitionLoc::Physical { start: 0 }],
label: String::new(),
root_icb: (0, 0),
}
}
#[test]
fn expand_directory_guards_reject_bad_directories() {
let volume = flat_volume();
let mut image = vec![0_u8; 8 * SS];
put(&mut image, 0, &fe(261, 5, 3, 4, b"file")); put(&mut image, SS, &vec![0xFF_u8; SS]); let mut cursor = std::io::Cursor::new(image);
assert!(
expand_directory(&mut cursor, &volume, "", 9, 0, Limits::DEFAULT)
.expect("ok")
.is_empty()
);
assert!(
expand_directory(&mut cursor, &volume, "", 0, 0, Limits::DEFAULT)
.expect("ok")
.is_empty()
);
assert!(
expand_directory(&mut cursor, &volume, "", 0, 1, Limits::DEFAULT)
.expect("ok")
.is_empty()
);
}
#[test]
fn directory_bytes_propagates_a_bad_continuation() {
let volume = flat_volume();
let buf = fe(261, 4, 0, 100, &sad(3, 16, 9000));
let entry = FileEntry::parse(&buf).expect("fe");
let mut cursor = std::io::Cursor::new(vec![0_u8; 4 * SS]);
assert!(directory_bytes(&mut cursor, &volume, &entry, 0, Limits::DEFAULT).is_err());
}
#[test]
fn file_body_propagates_a_bad_continuation() {
let volume = flat_volume();
let buf = fe(261, 5, 0, 100, &sad(3, 16, 9000));
let entry = FileEntry::parse(&buf).expect("fe");
let mut cursor = std::io::Cursor::new(vec![0_u8; 4 * SS]);
assert!(file_body(&mut cursor, &volume, &entry, 0, Limits::DEFAULT).is_err());
}
#[derive(Debug)]
struct FailIo {
fail_seek: bool,
}
impl Read for FailIo {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("read failed"))
}
}
impl Seek for FailIo {
fn seek(&mut self, _from: SeekFrom) -> std::io::Result<u64> {
if self.fail_seek { Err(std::io::Error::other("seek failed")) } else { Ok(0) }
}
}
#[test]
fn read_sector_io_error_arms() {
use super::read_sector_io;
let mut cursor = std::io::Cursor::new(vec![0_u8; SS]);
assert!(read_sector_io(&mut cursor, u64::MAX, 2048).is_err());
assert!(read_sector_io(&mut FailIo { fail_seek: true }, 0, 2048).is_err());
assert!(read_sector_io(&mut FailIo { fail_seek: false }, 0, 2048).is_err());
}
#[test]
fn read_runs_propagates_a_seek_failure() {
let runs = [Run { src: Some(0), len: 4 }];
assert!(read_runs(&mut FailIo { fail_seek: true }, &runs, 4).is_err());
}
#[test]
fn udf_file_reader_error_arms() {
let mut overflow = raw_reader(
Box::new(std::io::Cursor::new(vec![0_u8; 8])),
vec![Run { src: Some(u64::MAX), len: 10 }],
10,
1,
);
assert!(overflow.read(&mut [0_u8; 4]).is_err());
let mut seek_fail = raw_reader(
Box::new(FailIo { fail_seek: true }),
vec![Run { src: Some(0), len: 4 }],
4,
0,
);
assert!(seek_fail.read(&mut [0_u8; 4]).is_err());
let mut read_fail = raw_reader(
Box::new(FailIo { fail_seek: false }),
vec![Run { src: Some(0), len: 4 }],
4,
0,
);
assert!(read_fail.read(&mut [0_u8; 4]).is_err());
let mut empty_buf = raw_reader(
Box::new(FailIo { fail_seek: true }),
vec![Run { src: Some(0), len: 4 }],
4,
0,
);
assert_eq!(empty_buf.read(&mut []).expect("empty buffer short-circuits"), 0);
}
#[test]
fn read_extent_bytes_rejects_an_unresolvable_extent() {
use super::read_extent_bytes;
let locs = [PartitionLoc::Physical { start: 0 }];
let extent = Extent {
partition_ref: Some(9),
block: 0,
length: 16,
kind: ExtentKind::RecordedAllocated,
};
let mut cursor = std::io::Cursor::new(vec![0_u8; SS]);
assert!(read_extent_bytes(&mut cursor, &locs, 0, &extent, SS as u64, 16).is_err());
}
#[test]
fn metadata_sector_rejects_a_zero_block_size() {
let extents = [Extent {
partition_ref: None,
block: 1,
length: 2048,
kind: ExtentKind::RecordedAllocated,
}];
assert_eq!(metadata_sector(0, &extents, 0, 0), None);
}
#[derive(Debug)]
struct OpenOnce {
remaining: AtomicU32,
bytes: Arc<[u8]>,
}
impl IsoReader for OpenOnce {
fn open(&self) -> std::io::Result<Box<dyn super::ReadSeek>> {
if self.remaining.load(Ordering::Relaxed) == 0 {
return Err(std::io::Error::other("no more opens"));
}
self.remaining.fetch_sub(1, Ordering::Relaxed);
Ok(Box::new(std::io::Cursor::new(self.bytes.to_vec())))
}
}
#[test]
fn path_iso_open_propagates_a_missing_file() {
let factory = PathIso::new("no/such/bdinfo-rs-udf-xyzzy.iso");
assert!(factory.open().is_err());
assert!(
UdfSource::open(Box::new(PathIso::new("no/such/bdinfo-rs-udf-xyzzy.iso"))).is_err()
);
}
#[test]
fn open_read_and_open_text_propagate_a_factory_failure() {
let factory = OpenOnce { remaining: AtomicU32::new(1), bytes: Arc::from(physical_iso()) };
let src = UdfSource::open(Box::new(factory)).expect("open");
let files = src.root().get_files().expect("files");
let data = files.iter().find(|f| f.name() == "data.bin").expect("data.bin");
assert!(data.open_read().is_err());
assert!(data.open_text().is_err());
}
#[test]
fn dir_at_and_file_at_reject_out_of_range_indices() {
let src = open_physical();
assert!(super::dir_at(&src.inner, 999_999).is_none());
assert!(src.root().file_at(999_999).is_none());
}
#[test]
fn open_with_oversized_vds_fails_the_read() {
let mut iso = Iso::new(262);
iso.write(256, &avdp(257, 50 * SS as u32));
iso.write(257, &pd(0, 1, 1));
assert!(UdfSource::open(MemIso::boxed(iso.into_bytes())).is_err());
}
#[test]
fn open_with_unreadable_fsd_sector_fails() {
let mut iso = Iso::new(265);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 10));
iso.write(258, &lvd("X", SS as u32, 9000, 0, &phys_map(0), 1));
assert!(UdfSource::open(MemIso::boxed(iso.into_bytes())).is_err());
}
#[test]
fn open_with_unreadable_metadata_file_entry_fails() {
let lvd = Lvd {
logical_volume_identifier: String::new(),
logical_block_size: SS as u32,
file_set_descriptor: LongAd {
raw_length: 0,
location: LbAddr { block: 0, partition: 0 },
},
partition_maps: vec![
PartitionMap::Physical { partition_number: 0 },
PartitionMap::Metadata(MetadataPartitionMap {
physical_partition: 0,
metadata_file_location: 9000,
metadata_mirror_file_location: 3,
}),
],
};
let pds =
vec![PartitionDescriptor { partition_number: 0, starting_location: 0, length: 100 }];
let mut cursor = std::io::Cursor::new(vec![0_u8; 8 * SS]);
assert!(resolve_partitions(&mut cursor, &lvd, &pds, SS as u64, Limits::DEFAULT).is_err());
}
#[test]
fn open_with_a_bad_root_continuation_fails_the_build() {
let mut iso = Iso::new(270);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(0, 260, 50));
iso.write(258, &lvd("X", SS as u32, 1, 0, &phys_map(0), 1));
iso.write(261, &fsd(0, 2));
iso.write(262, &fe(261, 4, 0, 100, &sad(3, 16, 9000)));
assert!(UdfSource::open(MemIso::boxed(iso.into_bytes())).is_err());
}
#[test]
fn expand_directory_io_and_propagation_arms() {
let volume = flat_volume();
let mut image = vec![0_u8; 8 * SS];
put(&mut image, 2 * SS, &fe(261, 4, 0, 100, &sad(3, 16, 9000)));
let fids = dir_data(&[fid(0x0A, "", 3, 0), fid(0x00, "f", 4, 0)]);
put(&mut image, 3 * SS, &fe(261, 4, 3, fids.len() as u64, &fids));
put(&mut image, 4 * SS, &fe(261, 5, 0, 100, &sad(3, 16, 9000)));
let mut cursor = std::io::Cursor::new(image);
assert!(expand_directory(&mut cursor, &volume, "", 0, 9000, Limits::DEFAULT).is_err());
assert!(expand_directory(&mut cursor, &volume, "", 0, 2, Limits::DEFAULT).is_err());
assert!(expand_directory(&mut cursor, &volume, "", 0, 3, Limits::DEFAULT).is_err());
}
#[test]
fn directory_bytes_propagates_a_failed_extent_read() {
let volume = flat_volume();
let buf = fe(261, 4, 0, 2048, &sad(0, 2048, 9000));
let entry = FileEntry::parse(&buf).expect("fe");
let mut cursor = std::io::Cursor::new(vec![0_u8; 4 * SS]);
assert!(directory_bytes(&mut cursor, &volume, &entry, 0, Limits::DEFAULT).is_err());
}
#[test]
fn file_body_skips_an_unresolvable_extent() {
let volume = flat_volume();
let buf = fe(261, 5, 1, 64, &lad(0, 64, 0, 9));
let entry = FileEntry::parse(&buf).expect("fe");
let mut cursor = std::io::Cursor::new(vec![0_u8; 4 * SS]);
let body = file_body(&mut cursor, &volume, &entry, 0, Limits::DEFAULT).expect("body");
assert_eq!(body.length, 64);
}
#[test]
fn resolve_partitions_propagates_a_bad_metadata_continuation() {
let lvd = Lvd {
logical_volume_identifier: String::new(),
logical_block_size: SS as u32,
file_set_descriptor: LongAd {
raw_length: 0,
location: LbAddr { block: 0, partition: 0 },
},
partition_maps: vec![
PartitionMap::Physical { partition_number: 0 },
PartitionMap::Metadata(MetadataPartitionMap {
physical_partition: 0,
metadata_file_location: 2,
metadata_mirror_file_location: 3,
}),
],
};
let pds =
vec![PartitionDescriptor { partition_number: 0, starting_location: 0, length: 100 }];
let mut image = vec![0_u8; 8 * SS];
put(&mut image, 2 * SS, &fe(261, 250, 0, 100, &sad(3, 16, 9000)));
let mut cursor = std::io::Cursor::new(image);
assert!(resolve_partitions(&mut cursor, &lvd, &pds, SS as u64, Limits::DEFAULT).is_err());
}
#[test]
fn resolve_partitions_rejects_a_metadata_map_with_no_backing_partition() {
let lvd = Lvd {
logical_volume_identifier: String::new(),
logical_block_size: SS as u32,
file_set_descriptor: LongAd {
raw_length: 0,
location: LbAddr { block: 0, partition: 0 },
},
partition_maps: vec![PartitionMap::Metadata(MetadataPartitionMap {
physical_partition: 7,
metadata_file_location: 2,
metadata_mirror_file_location: 3,
})],
};
let pds =
vec![PartitionDescriptor { partition_number: 0, starting_location: 0, length: 100 }];
let mut cursor = std::io::Cursor::new(vec![0_u8; 8 * SS]);
assert!(resolve_partitions(&mut cursor, &lvd, &pds, SS as u64, Limits::DEFAULT).is_err());
}
#[test]
fn duplicate_fid_is_visited_once() {
let src = open_physical();
let files = src.root().get_files().expect("files");
assert_eq!(files.iter().filter(|f| f.name() == "data.bin" || f.name() == "dup").count(), 1);
}
fn minimal_iso_with_block_size(bs: u32) -> Vec<u8> {
let mut bytes = vec![0_u8; 262 * SS];
put(&mut bytes, 256 * SS, &avdp(257, 2 * SS as u32));
put(&mut bytes, 257 * SS, &pd(0, 2, 10));
put(&mut bytes, 258 * SS, &lvd("CAPBS", bs, 1, 0, &phys_map(0), 1));
let bs_us = bs as usize;
put(&mut bytes, 3 * bs_us, &fsd(0, 2));
put(&mut bytes, 4 * bs_us, &fe(261, 4, 3, 0, &[]));
bytes
}
#[test]
fn open_at_exactly_max_block_size_is_accepted() {
let src = UdfSource::open(MemIso::boxed(minimal_iso_with_block_size(32 << 10)))
.expect("open at the 32 KiB cap");
assert_eq!(src.volume_label(), "CAPBS");
assert!(file_names(&src.root()).is_empty());
}
#[test]
fn open_with_oversized_block_size_is_structure_not_found() {
let err = UdfSource::open(MemIso::boxed(minimal_iso_with_block_size(64 << 10)))
.expect_err("oversized block size");
assert_eq!(err.to_string(), "unable to locate BD structure");
}
#[test]
fn collect_extents_caps_direct_extents_at_max_extents() {
let mut ad = sad(0, 2048, 40);
ad.extend_from_slice(&sad(0, 2048, 41));
ad.extend_from_slice(&sad(0, 2048, 42));
let entry = FileEntry::parse(&fe(261, 5, 0, 6144, &ad)).expect("fe");
let locs = vec![PartitionLoc::Physical { start: 0 }];
let mut cursor = std::io::Cursor::new(vec![0_u8; 4 * SS]);
let limits = Limits { max_extents: 2, ..Limits::DEFAULT };
let extents =
collect_extents(&mut cursor, &locs, &entry, 0, SS as u64, limits).expect("extents");
assert_eq!(extents.iter().map(|e| e.block).collect::<Vec<_>>(), vec![40, 41]);
}
#[test]
fn collect_extents_caps_a_continuation_at_max_extents() {
let mut ads = sad(0, 2048, 50);
ads.extend_from_slice(&sad(0, 2048, 51));
ads.extend_from_slice(&sad(0, 2048, 52));
ads.extend_from_slice(&sad(0, 0, 0)); let mut image = vec![0_u8; 4 * SS];
put(&mut image, 2 * SS, &aed(&ads));
let mut cursor = std::io::Cursor::new(image);
let entry = FileEntry::parse(&fe(261, 5, 0, 6144, &sad(3, SS as u32, 2))).expect("fe");
let locs = vec![PartitionLoc::Physical { start: 0 }];
let limits = Limits { max_extents: 2, ..Limits::DEFAULT };
let extents =
collect_extents(&mut cursor, &locs, &entry, 0, SS as u64, limits).expect("extents");
assert_eq!(extents.iter().map(|e| e.block).collect::<Vec<_>>(), vec![50, 51]);
}
#[test]
fn collect_extents_clamps_a_hostile_continuation_length_to_one_block() {
let mut ads = sad(0, 4096, 77);
ads.extend_from_slice(&sad(0, 0, 0)); let mut image = vec![0_u8; 4 * SS];
put(&mut image, 2 * SS, &aed(&ads));
let mut cursor = std::io::Cursor::new(image);
let entry = FileEntry::parse(&fe(261, 5, 0, 4096, &sad(3, 0x3FFF_FFFF, 2))).expect("fe");
let locs = vec![PartitionLoc::Physical { start: 0 }];
let extents = collect_extents(&mut cursor, &locs, &entry, 0, SS as u64, Limits::DEFAULT)
.expect("extents");
assert_eq!(extents.iter().map(|e| e.block).collect::<Vec<_>>(), vec![77]);
}
#[test]
fn directory_bytes_clamps_each_extent_to_the_remaining_budget() {
let volume = flat_volume();
let entry =
FileEntry::parse(&fe(261, 4, 0, 0x3FFF_FFFF, &sad(0, 0x3FFF_FFFF, 1))).expect("fe");
let mut image = vec![0_u8; 4 * SS];
put(&mut image, SS, &[0xEE_u8; SS]);
let mut cursor = std::io::Cursor::new(image);
let limits = Limits { max_dir_bytes: 100, ..Limits::DEFAULT };
let bytes = directory_bytes(&mut cursor, &volume, &entry, 0, limits).expect("clamped");
assert_eq!(bytes, vec![0xEE_u8; 100]);
}
#[test]
fn directory_bytes_clamps_before_resolving_a_metadata_extent() {
let volume = Volume {
block_size: SS as u64,
locs: vec![PartitionLoc::Metadata {
phys_start: 0,
extents: vec![Extent {
partition_ref: None,
block: 1,
length: SS as u32,
kind: ExtentKind::RecordedAllocated,
}],
mirror_extents: Vec::new(),
}],
label: String::new(),
root_icb: (0, 0),
};
let entry =
FileEntry::parse(&fe(261, 4, 0, 0x3FFF_FFFF, &sad(0, 0x3FFF_FFFF, 0))).expect("fe");
let mut image = vec![0_u8; 4 * SS];
put(&mut image, SS, &[0xDD_u8; SS]);
let mut cursor = std::io::Cursor::new(image);
let limits = Limits { max_dir_bytes: 64, ..Limits::DEFAULT };
let bytes = directory_bytes(&mut cursor, &volume, &entry, 0, limits).expect("clamped");
assert_eq!(bytes, vec![0xDD_u8; 64]);
}
#[test]
fn read_extent_bytes_with_an_unbounded_cap_reads_the_full_extent() {
use super::read_extent_bytes;
let locs = [PartitionLoc::Physical { start: 0 }];
let extent = Extent {
partition_ref: None,
block: 1,
length: 16,
kind: ExtentKind::RecordedAllocated,
};
let mut image = vec![0_u8; 4 * SS];
put(&mut image, SS, b"0123456789ABCDEF");
let mut cursor = std::io::Cursor::new(image);
let bytes = read_extent_bytes(&mut cursor, &locs, 0, &extent, SS as u64, u64::MAX)
.expect("uncapped read");
assert_eq!(bytes, b"0123456789ABCDEF");
}
#[test]
fn file_body_caps_the_per_file_run_list() {
let volume = Volume {
block_size: SS as u64,
locs: vec![PartitionLoc::Metadata {
phys_start: 100,
extents: vec![
Extent {
partition_ref: None,
block: 10,
length: SS as u32,
kind: ExtentKind::RecordedAllocated,
},
Extent {
partition_ref: None,
block: 50,
length: SS as u32,
kind: ExtentKind::RecordedAllocated,
},
],
mirror_extents: Vec::new(),
}],
label: String::new(),
root_icb: (0, 0),
};
let entry =
FileEntry::parse(&fe(261, 5, 0, 2 * SS as u64, &sad(0, 2 * SS as u32, 0))).expect("fe");
let mut cursor = std::io::Cursor::new(vec![0_u8; SS]);
let limits = Limits { max_extents: 1, ..Limits::DEFAULT };
let body = file_body(&mut cursor, &volume, &entry, 0, limits).expect("body");
assert_eq!(
body.content,
Content::Runs(vec![Run { src: Some(110 * SS as u64), len: SS as u64 }])
);
}
#[test]
fn metadata_runs_coalesces_physically_adjacent_extents() {
let extents = vec![
Extent {
partition_ref: None,
block: 10,
length: SS as u32,
kind: ExtentKind::RecordedAllocated,
},
Extent {
partition_ref: None,
block: 11,
length: SS as u32,
kind: ExtentKind::RecordedAllocated,
},
];
let runs = metadata_runs(100, &extents, 0, 2 * SS as u64, SS as u64).expect("runs");
assert_eq!(runs, vec![Run { src: Some(110 * SS as u64), len: 2 * SS as u64 }]);
}
#[test]
fn metadata_runs_skips_extents_past_the_wanted_range() {
let extents = vec![
Extent {
partition_ref: None,
block: 10,
length: 2 * SS as u32,
kind: ExtentKind::RecordedAllocated,
},
Extent {
partition_ref: None,
block: 50,
length: SS as u32,
kind: ExtentKind::RecordedAllocated,
},
];
let runs = metadata_runs(100, &extents, 0, 2 * SS as u64, SS as u64).expect("runs");
assert_eq!(runs, vec![Run { src: Some(110 * SS as u64), len: 2 * SS as u64 }]);
}
#[test]
fn metadata_runs_rejects_a_zero_block_size() {
let extents = [Extent {
partition_ref: None,
block: 1,
length: 2048,
kind: ExtentKind::RecordedAllocated,
}];
assert_eq!(metadata_runs(0, &extents, 0, 2048, 0), None);
}
fn metadata_iso_with_nonzero_partition_number() -> Vec<u8> {
let mut iso = Iso::new(330);
iso.write(256, &avdp(257, 2 * SS as u32));
iso.write(257, &pd(5, 300, 100));
let mut maps = phys_map(5);
maps.extend_from_slice(&meta_map(5, 2, 3));
iso.write(258, &lvd("VOLNS", SS as u32, 1, 1, &maps, 2));
let mut meta_ads = sad(0, 3 * SS as u32, 10);
meta_ads.extend_from_slice(&sad(3, SS as u32, 4));
iso.write(302, &fe(261, 250, 0, 6 * SS as u64, &meta_ads));
let mut cont = sad(0, 3 * SS as u32, 13);
cont.extend_from_slice(&sad(0, 0, 0)); iso.write(304, &aed(&cont));
iso.write(311, &fsd(1, 2));
let root_fids = dir_data(&[fid(0x0A, "", 2, 1), fid(0x00, "FILE.BIN", 4, 1)]);
iso.write(312, &fe(261, 4, 0, root_fids.len() as u64, &sad(0, root_fids.len() as u32, 3)));
iso.write(313, &root_fids);
iso.write(314, &fe(261, 5, 1, 64, &lad(0, 64, 20, 0)));
iso.write(320, b"physical-partition-data-bytes-payload-0123456789ABCDEF0123456789");
iso.into_bytes()
}
#[test]
fn metadata_partition_number_distinct_from_reference_index_resolves() {
let src = UdfSource::open(MemIso::boxed(metadata_iso_with_nonzero_partition_number()))
.expect("open namespace iso");
assert_eq!(src.volume_label(), "VOLNS");
let root = src.root();
assert_eq!(file_names(&root), vec!["FILE.BIN".to_owned()]);
let files = root.get_files().expect("files");
let file = files.first().expect("FILE.BIN");
assert_eq!(
read_all(&**file),
b"physical-partition-data-bytes-payload-0123456789ABCDEF0123456789"
);
}
#[test]
fn resolve_partitions_rejects_a_metadata_map_without_a_physical_map() {
let lvd = Lvd {
logical_volume_identifier: String::new(),
logical_block_size: SS as u32,
file_set_descriptor: LongAd {
raw_length: 0,
location: LbAddr { block: 0, partition: 0 },
},
partition_maps: vec![PartitionMap::Metadata(MetadataPartitionMap {
physical_partition: 0,
metadata_file_location: 2,
metadata_mirror_file_location: 3,
})],
};
let pds =
vec![PartitionDescriptor { partition_number: 0, starting_location: 0, length: 100 }];
let mut cursor = std::io::Cursor::new(vec![0_u8; 8 * SS]);
assert!(resolve_partitions(&mut cursor, &lvd, &pds, SS as u64, Limits::DEFAULT).is_err());
}
#[derive(Debug, Clone)]
struct FaultyIso {
data: Arc<[u8]>,
bad: std::ops::Range<u64>,
}
impl FaultyIso {
fn boxed(bytes: Vec<u8>, bad: std::ops::Range<u64>) -> Box<dyn IsoReader> {
Box::new(Self { data: Arc::from(bytes), bad })
}
}
impl IsoReader for FaultyIso {
fn open(&self) -> std::io::Result<Box<dyn super::ReadSeek>> {
Ok(Box::new(FaultyReader {
cursor: std::io::Cursor::new(self.data.to_vec()),
bad: self.bad.clone(),
}))
}
}
struct FaultyReader {
cursor: std::io::Cursor<Vec<u8>>,
bad: std::ops::Range<u64>,
}
impl Read for FaultyReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let pos = self.cursor.position();
let end = pos.saturating_add(u64::try_from(buf.len()).unwrap_or(u64::MAX));
if pos < self.bad.end && end > self.bad.start {
return Err(std::io::Error::other("injected bad sector"));
}
self.cursor.read(buf)
}
}
impl Seek for FaultyReader {
fn seek(&mut self, from: SeekFrom) -> std::io::Result<u64> {
self.cursor.seek(from)
}
}
fn data_bin_first_extent() -> std::ops::Range<u64> {
(280 * SS as u64)..(281 * SS as u64)
}
#[test]
fn resilient_open_zero_fills_a_bad_sector_records_it_once_and_continues() {
let src =
UdfSource::open_resilient(FaultyIso::boxed(physical_iso(), data_bin_first_extent()))
.expect("the volume structures are intact, only file data is bad");
let root = src.root();
let files = root.get_files().expect("list root");
let data = files.iter().find(|f| f.name() == "data.bin").expect("data.bin");
let bytes = read_all(&**data);
assert_eq!(bytes.len(), SS + 100);
assert!(bytes.iter().take(SS).all(|&b| b == 0), "bad extent zero-filled");
assert!(bytes.iter().skip(SS).all(|&b| b == 0xCD), "good extent still read");
let errors = src.take_errors();
assert_eq!(errors.len(), 1);
let err = errors.first().expect("one recorded error");
assert_eq!(err.stage, ScanStage::SectorRead);
assert!(err.file.starts_with("data.bin @ byte 0"), "got {:?}", err.file);
assert!(err.reason.to_string().contains("injected bad sector"));
assert!(src.take_errors().is_empty());
let hide = files.iter().find(|f| f.name() == "hide.x").expect("hide.x");
assert_eq!(read_all(&**hide), b"0123456789");
assert!(src.take_errors().is_empty());
}
#[test]
fn strict_open_propagates_a_bad_sector_read() {
let src = UdfSource::open(FaultyIso::boxed(physical_iso(), data_bin_first_extent()))
.expect("volume structures intact");
let root = src.root();
let files = root.get_files().expect("list root");
let data = files.iter().find(|f| f.name() == "data.bin").expect("data.bin");
let mut reader = data.open_read().expect("open");
let mut sink = Vec::new();
let err = reader.read_to_end(&mut sink).expect_err("strict read fails");
assert!(err.to_string().contains("injected bad sector"));
assert!(src.take_errors().is_empty());
}
#[test]
fn a_damaged_handle_reports_only_its_first_unreadable_position() {
let src =
UdfSource::open_resilient(FaultyIso::boxed(physical_iso(), data_bin_first_extent()))
.expect("open resilient");
let root = src.root();
let files = root.get_files().expect("list root");
let data = files.iter().find(|f| f.name() == "data.bin").expect("data.bin");
let mut reader = data.open_read().expect("open");
let mut buf = [0xFF_u8; 16];
assert_eq!(reader.read(&mut buf).expect("first failing read"), 16);
assert_eq!(buf, [0_u8; 16]);
assert_eq!(reader.read(&mut buf).expect("second failing read"), 16);
let errors = src.take_errors();
assert_eq!(errors.len(), 1, "one record per damaged handle");
assert!(
errors.first().expect("recorded").file.ends_with("@ byte 0"),
"the FIRST failing position is the one recorded"
);
let mut again = data.open_read().expect("reopen");
let mut rest = Vec::new();
again.read_to_end(&mut rest).expect("resilient read completes");
assert_eq!(src.take_errors().len(), 1);
}
proptest! {
#[test]
fn open_never_panics_on_arbitrary_bytes(
data in proptest::collection::vec(any::<u8>(), 0..2048)
) {
drop(UdfSource::open(MemIso::boxed(data.clone())));
let mut image = vec![0_u8; 256 * SS];
image.extend_from_slice(&data);
drop(UdfSource::open(MemIso::boxed(image)));
}
#[test]
fn corrupted_valid_images_never_panic(
flips in proptest::collection::vec((0_usize..360 * SS, any::<u8>()), 0..16),
which in 0_usize..3,
) {
let mut bytes = match which {
0 => mirrored_metadata_iso(),
1 => metadata_iso_with_nonzero_partition_number(),
_ => vdp_iso(),
};
for &(pos, val) in &flips {
if let Some(slot) = bytes.get_mut(pos) {
*slot = val;
}
}
drop(UdfSource::open(MemIso::boxed(bytes.clone())));
if let Ok(src) = UdfSource::open_resilient(MemIso::boxed(bytes)) {
let mut sink = Vec::new();
for file in src.root().get_files().expect("arena listing is infallible") {
if let Ok(mut reader) = file.open_read() {
drop(reader.read_to_end(&mut sink));
}
}
drop(src.take_errors());
}
}
#[test]
fn resilient_reads_never_panic_under_arbitrary_fault_ranges(
start in 0_u64..(300 * SS as u64),
len in 0_u64..(40 * SS as u64),
) {
let bad = start..start.saturating_add(len);
if let Ok(src) = UdfSource::open(FaultyIso::boxed(physical_iso(), bad.clone())) {
let mut sink = Vec::new();
for file in src.root().get_files().expect("arena listing is infallible") {
if let Ok(mut reader) = file.open_read() {
drop(reader.read_to_end(&mut sink));
}
}
}
if let Ok(src) =
UdfSource::open(FaultyIso::boxed(mirrored_metadata_iso(), bad.clone()))
{
let mut sink = Vec::new();
for file in src.root().get_files().expect("arena listing is infallible") {
if let Ok(mut reader) = file.open_read() {
drop(reader.read_to_end(&mut sink));
}
}
}
if let Ok(src) = UdfSource::open_resilient(FaultyIso::boxed(physical_iso(), bad)) {
for file in src.root().get_files().expect("arena listing is infallible") {
let bytes = read_all(&*file);
prop_assert_eq!(bytes.len() as u64, file.length());
}
drop(src.take_errors());
}
}
}
}