use super::{LongAd, TAG_FILE_IDENTIFIER, Tag, cs0, u8_at, u16_le};
const FID_HIDDEN: u8 = 0x01;
const FID_DIRECTORY: u8 = 0x02;
const FID_DELETED: u8 = 0x04;
const FID_PARENT: u8 = 0x08;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fid {
pub characteristics: u8,
pub icb: LongAd,
pub name: String,
}
impl Fid {
#[must_use]
pub fn parse(buf: &[u8], offset: usize) -> Option<(Self, usize)> {
let tag = Tag::parse(buf, offset)?;
if tag.identifier != TAG_FILE_IDENTIFIER {
return None;
}
let characteristics = u8_at(buf, offset.saturating_add(18))?;
let l_fi = usize::from(u8_at(buf, offset.saturating_add(19))?);
let icb = LongAd::parse(buf, offset.saturating_add(20))?;
let l_iu = usize::from(u16_le(buf, offset.saturating_add(36))?);
let name_start = offset.saturating_add(38).saturating_add(l_iu);
let name_end = name_start.saturating_add(l_fi);
let name_bytes = buf.get(name_start..name_end)?;
let name = cs0::decode_dchars(name_bytes);
let raw_len = 38_usize.saturating_add(l_iu).saturating_add(l_fi);
let padded = raw_len.saturating_add(3) & !3_usize;
Some((Self { characteristics, icb, name }, padded))
}
#[must_use]
pub const fn is_directory(&self) -> bool {
self.characteristics & FID_DIRECTORY != 0
}
#[must_use]
pub const fn is_parent(&self) -> bool {
self.characteristics & FID_PARENT != 0
}
#[must_use]
pub const fn is_hidden(&self) -> bool {
self.characteristics & FID_HIDDEN != 0
}
#[must_use]
pub const fn is_deleted(&self) -> bool {
self.characteristics & FID_DELETED != 0
}
}
#[must_use]
pub fn parse_directory(buf: &[u8]) -> Vec<Fid> {
let mut fids = Vec::new();
let mut off: usize = 0;
while let Some((fid, len)) = Fid::parse(buf, off) {
fids.push(fid);
off = off.saturating_add(len);
}
fids
}
#[cfg(test)]
mod tests {
use super::{Fid, parse_directory};
use crate::vfs::udf::LbAddr;
fn make_fid(
characteristics: u8,
name: &[u8],
l_iu: usize,
block: u32,
partition: u16,
) -> Vec<u8> {
let raw = 38_usize.saturating_add(l_iu).saturating_add(name.len());
let padded = raw.saturating_add(3) & !3_usize;
let mut buf = vec![0_u8; padded];
put(&mut buf, 0, &super::TAG_FILE_IDENTIFIER.to_le_bytes());
put(&mut buf, 2, &3_u16.to_le_bytes());
put(&mut buf, 18, &[characteristics]);
put(&mut buf, 19, &[u8::try_from(name.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, &partition.to_le_bytes());
put(&mut buf, 36, &u16::try_from(l_iu).unwrap_or(0).to_le_bytes());
put(&mut buf, 38_usize.saturating_add(l_iu), name);
fix_tag_checksum(&mut buf);
buf
}
fn put(buf: &mut [u8], off: usize, bytes: &[u8]) {
for (dst, &src) in buf.iter_mut().skip(off).zip(bytes) {
*dst = src;
}
}
fn fix_tag_checksum(buf: &mut [u8]) {
let sum = buf
.iter()
.take(16)
.enumerate()
.filter(|&(i, _)| i != 4)
.fold(0_u8, |acc, (_, &b)| acc.wrapping_add(b));
put(buf, 4, &[sum]);
}
#[test]
fn fid_parses_directory_child_with_latin1_name() {
let buf = make_fid(0x02, &[8, b'B', b'D', b'M', b'V'], 0, 0x12, 0);
let (fid, len) = Fid::parse(&buf, 0).expect("fid");
assert_eq!(fid.name, "BDMV");
assert!(fid.is_directory());
assert!(!fid.is_parent());
assert!(!fid.is_hidden());
assert!(!fid.is_deleted());
assert_eq!(fid.icb.location, LbAddr { block: 0x12, partition: 0 });
assert_eq!(len, buf.len());
assert_eq!(len, 44);
}
#[test]
fn fid_parses_utf16_name_and_implementation_use() {
let buf = make_fid(0x00, &[16, 0x00, b'A', 0x00, 0xE9], 4, 0x20, 1);
let (fid, _len) = Fid::parse(&buf, 0).expect("fid");
assert_eq!(fid.name, "Aé");
assert!(!fid.is_directory());
assert_eq!(fid.icb.location, LbAddr { block: 0x20, partition: 1 });
}
#[test]
fn fid_flags_decode_each_bit() {
let all = make_fid(0x0F, &[], 0, 0, 0);
let (fid, _) = Fid::parse(&all, 0).expect("fid");
assert!(fid.is_hidden());
assert!(fid.is_directory());
assert!(fid.is_deleted());
assert!(fid.is_parent());
assert_eq!(fid.name, "");
let none = make_fid(0x00, &[8, b'X'], 0, 0, 0);
let (fid, _) = Fid::parse(&none, 0).expect("fid");
assert!(!fid.is_hidden());
assert!(!fid.is_directory());
assert!(!fid.is_deleted());
assert!(!fid.is_parent());
}
#[test]
fn fid_rejects_wrong_tag() {
let mut buf = make_fid(0x02, &[8, b'X'], 0, 0, 0);
put(&mut buf, 0, &261_u16.to_le_bytes());
fix_tag_checksum(&mut buf);
assert_eq!(Fid::parse(&buf, 0), None);
}
#[test]
fn fid_parse_is_none_for_every_truncation() {
let full = make_fid(0x02, &[8, b'B', b'D', b'M', b'V'], 0, 0x10, 0);
for len in [0_usize, 8, 16, 18, 19, 20, 30, 36, 38, 42] {
let truncated = full.get(..len).unwrap_or_default();
assert_eq!(Fid::parse(truncated, 0), None, "truncation to {len} bytes");
}
assert!(Fid::parse(&full, 0).is_some());
assert_eq!(Fid::parse(&full, usize::MAX), None);
}
#[test]
fn parse_directory_enumerates_parent_then_children() {
let mut dir = Vec::new();
dir.extend_from_slice(&make_fid(0x0A, &[], 0, 0x05, 0)); dir.extend_from_slice(&make_fid(0x02, &[8, b'B', b'D', b'M', b'V'], 0, 0x12, 0));
dir.extend_from_slice(&make_fid(0x00, &[8, b'M', b'O', b'V', b'I', b'E'], 0, 0x40, 0));
dir.extend_from_slice(&[0_u8; 8]);
let fids = parse_directory(&dir);
assert_eq!(fids.len(), 3);
assert!(fids.first().is_some_and(Fid::is_parent));
let names: Vec<&str> = fids.iter().map(|f| f.name.as_str()).collect();
assert_eq!(names, ["", "BDMV", "MOVIE"]);
assert_eq!(fids.get(1).map(|f| f.icb.location.block), Some(0x12));
assert_eq!(fids.get(2).map(|f| f.icb.location.block), Some(0x40));
}
#[test]
fn parse_directory_empty_input_is_empty() {
assert!(parse_directory(&[]).is_empty());
}
#[test]
fn parse_directory_stops_after_last_valid_fid() {
let dir = make_fid(0x02, &[8, b'A'], 0, 1, 0);
let fids = parse_directory(&dir);
assert_eq!(fids.len(), 1);
assert_eq!(fids.first().map(|f| f.name.clone()), Some("A".to_owned()));
}
}