use super::{
EXTENT_LENGTH_MASK, Extent, ExtentKind, LbAddr, LongAd, ShortAd, TAG_EXTENDED_FILE_ENTRY,
TAG_FILE_ENTRY, Tag, as_offset, u8_at, u16_le, u32_le, u64_le,
};
const FILE_TYPE_DIRECTORY: u8 = 4;
const ICB_STRATEGY_DIRECT: u16 = 4;
const EXT_AD_LEN: usize = 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllocationType {
Short,
Long,
Extended,
Embedded,
Reserved,
}
impl AllocationType {
const fn from_flags(flags: u16) -> Self {
match flags & 0x7 {
0 => Self::Short,
1 => Self::Long,
2 => Self::Extended,
3 => Self::Embedded,
_ => Self::Reserved,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IcbTag {
pub strategy_type: u16,
pub file_type: u8,
pub allocation_type: AllocationType,
}
impl IcbTag {
#[must_use]
pub fn parse(buf: &[u8], offset: usize) -> Option<Self> {
let strategy_type = u16_le(buf, offset.saturating_add(4))?;
let file_type = u8_at(buf, offset.saturating_add(11))?;
let flags = u16_le(buf, offset.saturating_add(18))?;
Some(Self { strategy_type, file_type, allocation_type: AllocationType::from_flags(flags) })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileData {
Extents(Vec<Extent>),
Embedded(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileEntry {
pub information_length: u64,
pub icb_tag: IcbTag,
pub data: FileData,
}
impl FileEntry {
#[must_use]
pub fn parse(buf: &[u8]) -> Option<Self> {
let tag = Tag::parse(buf, 0)?;
let (l_ea_off, l_ad_off, ad_base) = match tag.identifier {
TAG_FILE_ENTRY => (168_usize, 172_usize, 176_usize),
TAG_EXTENDED_FILE_ENTRY => (208_usize, 212_usize, 216_usize),
_ => return None,
};
let icb_tag = IcbTag::parse(buf, 16)?;
if icb_tag.strategy_type != ICB_STRATEGY_DIRECT {
return None;
}
let information_length = u64_le(buf, 56)?;
let l_ea = as_offset(u32_le(buf, l_ea_off)?);
let l_ad = as_offset(u32_le(buf, l_ad_off)?);
let ad_start = ad_base.saturating_add(l_ea);
let ad_end = ad_start.saturating_add(l_ad);
let ad_area = buf.get(ad_start..ad_end)?;
let data = match icb_tag.allocation_type {
AllocationType::Embedded => FileData::Embedded(ad_area.to_vec()),
other => FileData::Extents(parse_allocation_area(ad_area, other)),
};
Some(Self { information_length, icb_tag, data })
}
#[must_use]
pub const fn is_directory(&self) -> bool {
self.icb_tag.file_type == FILE_TYPE_DIRECTORY
}
#[must_use]
pub fn extents(&self) -> &[Extent] {
match &self.data {
FileData::Extents(extents) => extents,
FileData::Embedded(_) => &[],
}
}
#[must_use]
pub fn embedded_data(&self) -> Option<&[u8]> {
match &self.data {
FileData::Embedded(bytes) => Some(bytes),
FileData::Extents(_) => None,
}
}
}
fn short_extent(area: &[u8], off: usize) -> Option<Extent> {
let ad = ShortAd::parse(area, off)?;
Some(Extent {
partition_ref: None,
block: ad.position,
length: ad.length_bytes(),
kind: ad.kind(),
})
}
fn long_extent(area: &[u8], off: usize) -> Option<Extent> {
let ad = LongAd::parse(area, off)?;
Some(Extent {
partition_ref: Some(ad.location.partition),
block: ad.location.block,
length: ad.length_bytes(),
kind: ad.kind(),
})
}
fn ext_extent(area: &[u8], off: usize) -> Option<Extent> {
let raw_length = u32_le(area, off)?;
let location = LbAddr::parse(area, off.saturating_add(12))?;
Some(Extent {
partition_ref: Some(location.partition),
block: location.block,
length: raw_length & EXTENT_LENGTH_MASK,
kind: ExtentKind::from_raw(raw_length),
})
}
#[must_use]
pub fn parse_allocation_area(area: &[u8], allocation_type: AllocationType) -> Vec<Extent> {
match allocation_type {
AllocationType::Short => parse_ads(area, ShortAd::LEN, short_extent),
AllocationType::Long => parse_ads(area, LongAd::LEN, long_extent),
AllocationType::Extended => parse_ads(area, EXT_AD_LEN, ext_extent),
AllocationType::Embedded | AllocationType::Reserved => Vec::new(),
}
}
fn parse_ads(
area: &[u8],
step: usize,
read: impl Fn(&[u8], usize) -> Option<Extent>,
) -> Vec<Extent> {
let mut extents = Vec::new();
let mut off: usize = 0;
while let Some(extent) = read(area, off) {
if extent.length == 0 {
break;
}
extents.push(extent);
off = off.saturating_add(step);
}
extents
}
#[cfg(test)]
mod tests {
use super::{AllocationType, FileData, FileEntry, IcbTag};
use crate::vfs::udf::{Extent, ExtentKind};
fn put(buf: &mut [u8], off: usize, bytes: &[u8]) {
for (dst, &src) in buf.iter_mut().skip(off).zip(bytes) {
*dst = src;
}
}
fn file_entry(size: usize, identifier: u16, file_type: u8, flags: u16) -> Vec<u8> {
let mut buf = vec![0_u8; size];
put(&mut buf, 0, &identifier.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());
let sum = buf
.iter()
.take(16)
.enumerate()
.filter(|&(i, _)| i != 4)
.fold(0_u8, |acc, (_, &b)| acc.wrapping_add(b));
put(&mut buf, 4, &[sum]);
buf
}
#[test]
fn icb_tag_decodes_each_allocation_type() {
for (flags, expected) in [
(0_u16, AllocationType::Short),
(1, AllocationType::Long),
(2, AllocationType::Extended),
(3, AllocationType::Embedded),
(4, AllocationType::Reserved),
(0x0108, AllocationType::Short),
] {
let buf = file_entry(64, super::TAG_FILE_ENTRY, 5, flags);
let icb = IcbTag::parse(&buf, 16).expect("icb tag");
assert_eq!(icb.allocation_type, expected);
assert_eq!(icb.file_type, 5);
assert_eq!(icb.strategy_type, 4);
}
}
#[test]
fn icb_tag_reads_any_strategy_type() {
let mut buf = file_entry(64, super::TAG_FILE_ENTRY, 5, 0);
put(&mut buf, 20, &4096_u16.to_le_bytes());
assert_eq!(IcbTag::parse(&buf, 16).expect("icb tag").strategy_type, 4096);
}
#[test]
fn file_entry_rejects_a_non_direct_strategy() {
let good = file_entry(256, super::TAG_FILE_ENTRY, 5, 0);
assert!(FileEntry::parse(&good).is_some());
for strategy in [0_u16, 1, 4096, u16::MAX] {
let mut buf = good.clone();
put(&mut buf, 20, &strategy.to_le_bytes());
assert_eq!(FileEntry::parse(&buf), None, "strategy {strategy}");
}
}
#[test]
fn icb_tag_short_buffer_is_none() {
let buf = file_entry(20, super::TAG_FILE_ENTRY, 4, 0);
assert_eq!(IcbTag::parse(&buf, 16), None);
}
#[test]
fn file_entry_short_ad_extents_with_terminator() {
let mut buf = file_entry(256, super::TAG_FILE_ENTRY, 4, 0);
put(&mut buf, 56, &4096_u64.to_le_bytes()); put(&mut buf, 168, &0_u32.to_le_bytes()); put(&mut buf, 172, &24_u32.to_le_bytes()); put(&mut buf, 176, &0x800_u32.to_le_bytes());
put(&mut buf, 180, &0x10_u32.to_le_bytes());
put(&mut buf, 184, &0x800_u32.to_le_bytes());
put(&mut buf, 188, &0x11_u32.to_le_bytes());
let fe = FileEntry::parse(&buf).expect("file entry");
assert!(fe.is_directory());
assert_eq!(fe.information_length, 4096);
assert_eq!(fe.embedded_data(), None);
assert_eq!(
fe.extents(),
&[
Extent {
partition_ref: None,
block: 0x10,
length: 0x800,
kind: ExtentKind::RecordedAllocated
},
Extent {
partition_ref: None,
block: 0x11,
length: 0x800,
kind: ExtentKind::RecordedAllocated
},
]
);
}
#[test]
fn file_entry_short_ad_stops_at_area_end_without_terminator() {
let mut buf = file_entry(256, super::TAG_FILE_ENTRY, 5, 0);
put(&mut buf, 172, &8_u32.to_le_bytes());
put(&mut buf, 176, &0x1000_u32.to_le_bytes());
put(&mut buf, 180, &0x20_u32.to_le_bytes());
let fe = FileEntry::parse(&buf).expect("file entry");
assert!(!fe.is_directory());
assert_eq!(
fe.extents(),
&[Extent {
partition_ref: None,
block: 0x20,
length: 0x1000,
kind: ExtentKind::RecordedAllocated
}]
);
}
#[test]
fn file_entry_long_ad_extents_carry_partition() {
let mut buf = file_entry(256, super::TAG_FILE_ENTRY, 5, 1);
put(&mut buf, 172, &16_u32.to_le_bytes()); put(&mut buf, 176, &0x800_u32.to_le_bytes()); put(&mut buf, 180, &0x30_u32.to_le_bytes()); put(&mut buf, 184, &2_u16.to_le_bytes()); let fe = FileEntry::parse(&buf).expect("file entry");
assert_eq!(
fe.extents(),
&[Extent {
partition_ref: Some(2),
block: 0x30,
length: 0x800,
kind: ExtentKind::RecordedAllocated
}]
);
}
#[test]
fn file_entry_ext_ad_extents() {
let mut buf = file_entry(256, super::TAG_FILE_ENTRY, 5, 2);
put(&mut buf, 172, &20_u32.to_le_bytes()); put(&mut buf, 176, &0x800_u32.to_le_bytes()); put(&mut buf, 188, &0x44_u32.to_le_bytes()); put(&mut buf, 192, &1_u16.to_le_bytes()); let fe = FileEntry::parse(&buf).expect("file entry");
assert_eq!(
fe.extents(),
&[Extent {
partition_ref: Some(1),
block: 0x44,
length: 0x800,
kind: ExtentKind::RecordedAllocated
}]
);
}
#[test]
fn file_entry_embedded_exposes_inline_bytes() {
let mut buf = file_entry(256, super::TAG_FILE_ENTRY, 4, 3);
put(&mut buf, 172, &5_u32.to_le_bytes()); put(&mut buf, 176, &[0xDE, 0xAD, 0xBE, 0xEF, 0x42]);
let fe = FileEntry::parse(&buf).expect("file entry");
assert!(fe.is_directory());
assert_eq!(fe.embedded_data(), Some([0xDE_u8, 0xAD, 0xBE, 0xEF, 0x42].as_slice()));
assert_eq!(fe.extents(), &[]);
}
#[test]
fn file_entry_reserved_allocation_type_has_no_extents() {
let mut buf = file_entry(256, super::TAG_FILE_ENTRY, 5, 4);
put(&mut buf, 172, &8_u32.to_le_bytes());
let fe = FileEntry::parse(&buf).expect("file entry");
assert_eq!(fe.extents(), &[]);
assert_eq!(fe.embedded_data(), None);
}
#[test]
fn extended_file_entry_uses_its_own_offsets() {
let mut buf = file_entry(512, super::TAG_EXTENDED_FILE_ENTRY, 5, 0);
put(&mut buf, 56, &9000_u64.to_le_bytes());
put(&mut buf, 208, &4_u32.to_le_bytes()); put(&mut buf, 212, &8_u32.to_le_bytes()); put(&mut buf, 220, &0x900_u32.to_le_bytes()); put(&mut buf, 224, &0x55_u32.to_le_bytes());
let fe = FileEntry::parse(&buf).expect("efe");
assert_eq!(fe.information_length, 9000);
assert_eq!(
fe.extents(),
&[Extent {
partition_ref: None,
block: 0x55,
length: 0x900,
kind: ExtentKind::RecordedAllocated
}]
);
}
#[test]
fn next_extent_descriptor_is_kept_with_its_kind() {
let mut buf = file_entry(256, super::TAG_FILE_ENTRY, 4, 0);
put(&mut buf, 172, &8_u32.to_le_bytes());
put(&mut buf, 176, &0xC000_0800_u32.to_le_bytes()); put(&mut buf, 180, &0x70_u32.to_le_bytes());
let fe = FileEntry::parse(&buf).expect("file entry");
assert_eq!(
fe.extents(),
&[Extent {
partition_ref: None,
block: 0x70,
length: 0x800,
kind: ExtentKind::NextExtent
}]
);
}
#[test]
fn file_entry_ext_ad_truncated_descriptor_yields_no_extents() {
let mut buf = file_entry(256, super::TAG_FILE_ENTRY, 5, 2);
put(&mut buf, 172, &14_u32.to_le_bytes());
let fe = FileEntry::parse(&buf).expect("file entry");
assert_eq!(fe.extents(), &[]);
}
#[test]
fn file_entry_none_for_all_truncations() {
let full = file_entry(176, super::TAG_FILE_ENTRY, 4, 0);
assert!(FileEntry::parse(&full).is_some());
for len in 0..176 {
assert_eq!(FileEntry::parse(full.get(..len).unwrap_or_default()), None, "fe {len}");
}
}
#[test]
fn file_entry_rejects_wrong_tag() {
let buf = file_entry(256, crate::vfs::udf::TAG_FILE_IDENTIFIER, 5, 0);
assert_eq!(FileEntry::parse(&buf), None);
}
#[test]
fn file_entry_rejects_ad_area_past_buffer() {
let mut buf = file_entry(180, super::TAG_FILE_ENTRY, 5, 0);
put(&mut buf, 172, &1000_u32.to_le_bytes());
assert_eq!(FileEntry::parse(&buf), None);
}
#[test]
fn file_entry_short_buffer_before_icb_is_none() {
let buf = file_entry(20, super::TAG_FILE_ENTRY, 5, 0);
assert_eq!(FileEntry::parse(&buf), None);
}
#[test]
fn parse_allocation_area_handles_each_form() {
use super::parse_allocation_area;
use crate::vfs::udf::ExtentKind;
let mut short = Vec::new();
short.extend_from_slice(&0x800_u32.to_le_bytes()); short.extend_from_slice(&0x90_u32.to_le_bytes()); short.extend_from_slice(&[0_u8; 8]); assert_eq!(
parse_allocation_area(&short, AllocationType::Short),
vec![Extent {
partition_ref: None,
block: 0x90,
length: 0x800,
kind: ExtentKind::RecordedAllocated
}]
);
let mut long = Vec::new();
long.extend_from_slice(&0x800_u32.to_le_bytes());
long.extend_from_slice(&0x10_u32.to_le_bytes()); long.extend_from_slice(&3_u16.to_le_bytes()); long.extend_from_slice(&[0_u8; 6]); assert_eq!(
parse_allocation_area(&long, AllocationType::Long).first().map(|e| e.partition_ref),
Some(Some(3))
);
let mut ext = vec![0_u8; 20];
put(&mut ext, 0, &0x800_u32.to_le_bytes());
put(&mut ext, 12, &0x44_u32.to_le_bytes()); put(&mut ext, 16, &1_u16.to_le_bytes()); assert_eq!(parse_allocation_area(&ext, AllocationType::Extended).len(), 1);
assert!(parse_allocation_area(&short, AllocationType::Embedded).is_empty());
assert!(parse_allocation_area(&short, AllocationType::Reserved).is_empty());
}
#[test]
fn file_data_variants_are_equatable() {
assert_eq!(FileData::Embedded(vec![1, 2]), FileData::Embedded(vec![1, 2]));
assert_ne!(FileData::Embedded(vec![1]), FileData::Extents(vec![]));
}
}