use std::io::{Read, Seek, SeekFrom};
const TAG_AVDP: u16 = 2;
const TAG_PD: u16 = 5;
const TAG_LVD: u16 = 6;
const TAG_TERM: u16 = 8;
const TAG_FSD: u16 = 256;
const TAG_FID: u16 = 257;
const TAG_FE: u16 = 260;
const TAG_FE_ALT: u16 = 261;
const TAG_EFE: u16 = 266;
const FC_DIRECTORY: u8 = 0x02;
const FC_PARENT: u8 = 0x08;
const ALLOC_SHORT: u16 = 0;
const ALLOC_LONG: u16 = 1;
const ALLOC_INLINE: u16 = 3;
const EXTENT_RECORDED: u32 = 0x0000_0000;
#[derive(Debug, Clone)]
pub struct UdfFileEntry {
pub name: String,
pub is_dir: bool,
pub size: u64,
pub fe_lba: u32,
}
pub(crate) struct UdfState {
pub partition_start: u32,
pub root_fe_lba: u32,
}
pub fn detect_udf<R: Read + Seek>(reader: &mut R) -> bool {
let mut buf = [0u8; 6];
for lba in 16u64..32 {
let pos = lba * 2048 + 1;
if reader.seek(SeekFrom::Start(pos)).is_err() {
break;
}
if reader.read_exact(&mut buf).is_err() {
break;
}
let id = &buf[..5];
if id == b"NSR02" || id == b"NSR03" {
return true;
}
if id == b"TEA01" {
break;
}
}
false
}
pub(crate) fn parse_udf_state<R: Read + Seek>(reader: &mut R) -> Option<UdfState> {
let (vds_loc, vds_len) = read_avdp(reader)?;
let (partition_start, fsd_lba) = read_vds(reader, vds_loc, vds_len)?;
let root_fe_lba = read_fsd(reader, fsd_lba, partition_start)?;
Some(UdfState { partition_start, root_fe_lba })
}
pub(crate) fn read_dir_at_lba<R: Read + Seek>(
reader: &mut R,
partition_start: u32,
dir_fe_lba: u32,
) -> Option<Vec<UdfFileEntry>> {
let dir_data = read_fe_data(reader, partition_start, dir_fe_lba)?;
Some(parse_fids(reader, partition_start, &dir_data))
}
pub(crate) fn read_fe_data<R: Read + Seek>(
reader: &mut R,
partition_start: u32,
fe_lba: u32,
) -> Option<Vec<u8>> {
let mut sector = [0u8; 2048];
seek_read(reader, fe_lba as u64 * 2048, &mut sector)?;
let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
let is_efe = tag_ident == TAG_EFE;
if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && !is_efe {
return None;
}
let icb_flags = u16::from_le_bytes([sector[34], sector[35]]);
let alloc_type = icb_flags & 0x0007;
let info_len = u64::from_le_bytes(sector[56..64].try_into().unwrap());
let (ea_off, ad_off, header) = if is_efe {
(176usize, 180usize, 184usize)
} else {
(168usize, 172usize, 176usize)
};
if ad_off + 4 > sector.len() {
return None;
}
let ea_len = u32::from_le_bytes(sector[ea_off..ea_off + 4].try_into().unwrap()) as usize;
let ad_len = u32::from_le_bytes(sector[ad_off..ad_off + 4].try_into().unwrap()) as usize;
let ad_start = header + ea_len;
let ad_end = ad_start + ad_len;
if ad_end > sector.len() {
return None;
}
let ad_area = sector[ad_start..ad_end].to_vec();
match alloc_type {
ALLOC_INLINE => Some(ad_area[..info_len.min(ad_area.len() as u64) as usize].to_vec()),
ALLOC_SHORT => read_extents_short(reader, partition_start, &ad_area, info_len),
ALLOC_LONG => read_extents_long(reader, partition_start, &ad_area, info_len),
_ => None,
}
}
fn read_avdp<R: Read + Seek>(reader: &mut R) -> Option<(u32, u32)> {
let mut sector = [0u8; 2048];
seek_read(reader, 256 * 2048, &mut sector)?;
if u16::from_le_bytes([sector[0], sector[1]]) != TAG_AVDP {
return None;
}
let vds_len = u32::from_le_bytes(sector[16..20].try_into().unwrap());
let vds_loc = u32::from_le_bytes(sector[20..24].try_into().unwrap());
Some((vds_loc, vds_len))
}
fn read_vds<R: Read + Seek>(
reader: &mut R,
vds_loc: u32,
vds_len: u32,
) -> Option<(u32, u32)> {
let sectors = (vds_len as usize).div_ceil(2048);
let mut partition_start: Option<u32> = None;
let mut fsd_lbn: Option<u32> = None;
for i in 0..sectors {
let mut sector = [0u8; 2048];
seek_read(reader, (vds_loc as u64 + i as u64) * 2048, &mut sector)?;
let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
match tag_ident {
TAG_PD => {
let psl = u32::from_le_bytes(sector[188..192].try_into().unwrap());
partition_start = Some(psl);
}
TAG_LVD => {
let lbn = u32::from_le_bytes(sector[252..256].try_into().unwrap());
fsd_lbn = Some(lbn);
}
TAG_TERM | 0 => break,
_ => {}
}
}
let ps = partition_start?;
let fsd = fsd_lbn?;
Some((ps, ps + fsd))
}
fn read_fsd<R: Read + Seek>(
reader: &mut R,
fsd_lba: u32,
partition_start: u32,
) -> Option<u32> {
let mut sector = [0u8; 2048];
seek_read(reader, fsd_lba as u64 * 2048, &mut sector)?;
if u16::from_le_bytes([sector[0], sector[1]]) != TAG_FSD {
return None;
}
let lbn = u32::from_le_bytes(sector[404..408].try_into().unwrap());
Some(partition_start + lbn)
}
fn detect_fid_tag_size(data: &[u8]) -> usize {
let mut off = 0;
while off + 28 <= data.len() {
let ti = u16::from_le_bytes([data[off], data[off + 1]]);
if ti == TAG_FID {
let lbn16 = if off + 26 <= data.len() {
u32::from_le_bytes(data[off + 22..off + 26].try_into().unwrap())
} else {
u32::MAX
};
let lbn18 = if off + 28 <= data.len() {
u32::from_le_bytes(data[off + 24..off + 28].try_into().unwrap())
} else {
u32::MAX
};
if lbn16 < 0x10000 {
return 16;
}
if lbn18 < 0x10000 {
return 18;
}
return 16; }
off += 4;
}
16
}
fn parse_fids<R: Read + Seek>(
reader: &mut R,
partition_start: u32,
data: &[u8],
) -> Vec<UdfFileEntry> {
let tag_size = detect_fid_tag_size(data);
let min_fid = tag_size + 20;
let mut entries = Vec::new();
let mut off = 0;
while off + min_fid <= data.len() {
let tag_ident = u16::from_le_bytes([data[off], data[off + 1]]);
if tag_ident != TAG_FID {
off += 4;
continue;
}
let crc_len = u16::from_le_bytes([data[off + 10], data[off + 11]]) as usize;
let fid_advance = ((16 + crc_len + 3) & !3).max(min_fid);
if off + fid_advance > data.len() {
break;
}
let file_chars = data[off + tag_size];
let file_id_len = data[off + tag_size + 1] as usize;
let icb_lbn = if off + tag_size + 10 <= data.len() {
u32::from_le_bytes(
data[off + tag_size + 6..off + tag_size + 10].try_into().unwrap(),
)
} else {
off += fid_advance.max(4);
continue;
};
let impl_use_len = if off + tag_size + 20 <= data.len() {
u16::from_le_bytes([data[off + tag_size + 18], data[off + tag_size + 19]]) as usize
} else {
off += fid_advance.max(4);
continue;
};
if file_chars & FC_PARENT == 0 {
let is_dir = file_chars & FC_DIRECTORY != 0;
let fe_lba = partition_start + icb_lbn;
let id_start = off + tag_size + 20 + impl_use_len;
let id_end = (id_start + file_id_len).min(data.len());
let name = if id_end > id_start {
decode_osta_cs0(&data[id_start..id_end])
} else {
String::new()
};
let size = read_fe_info_len(reader, fe_lba).unwrap_or(0);
entries.push(UdfFileEntry { name, is_dir, size, fe_lba });
}
off += fid_advance.max(4);
}
entries
}
fn read_fe_info_len<R: Read + Seek>(reader: &mut R, fe_lba: u32) -> Option<u64> {
let mut sector = [0u8; 2048];
seek_read(reader, fe_lba as u64 * 2048, &mut sector)?;
let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && tag_ident != TAG_EFE {
return None;
}
Some(u64::from_le_bytes(sector[56..64].try_into().unwrap()))
}
fn read_extents_short<R: Read + Seek>(
reader: &mut R,
partition_start: u32,
ad_area: &[u8],
total_len: u64,
) -> Option<Vec<u8>> {
let mut data = Vec::new();
let mut pos = 0;
while pos + 8 <= ad_area.len() && (data.len() as u64) < total_len {
let len_raw = u32::from_le_bytes(ad_area[pos..pos + 4].try_into().unwrap());
let ext_pos = u32::from_le_bytes(ad_area[pos + 4..pos + 8].try_into().unwrap());
let ext_type = len_raw >> 30;
let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
let phys = (partition_start as u64 + ext_pos as u64) * 2048;
read_extent(reader, phys, ext_len, total_len, &mut data)?;
}
pos += 8;
}
data.truncate(total_len as usize);
Some(data)
}
fn read_extents_long<R: Read + Seek>(
reader: &mut R,
partition_start: u32,
ad_area: &[u8],
total_len: u64,
) -> Option<Vec<u8>> {
let mut data = Vec::new();
let mut pos = 0;
while pos + 16 <= ad_area.len() && (data.len() as u64) < total_len {
let len_raw = u32::from_le_bytes(ad_area[pos..pos + 4].try_into().unwrap());
let lbn = u32::from_le_bytes(ad_area[pos + 4..pos + 8].try_into().unwrap());
let ext_type = len_raw >> 30;
let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
let phys = (partition_start as u64 + lbn as u64) * 2048;
read_extent(reader, phys, ext_len, total_len, &mut data)?;
}
pos += 16;
}
data.truncate(total_len as usize);
Some(data)
}
fn read_extent<R: Read + Seek>(
reader: &mut R,
byte_pos: u64,
ext_len: usize,
total_len: u64,
data: &mut Vec<u8>,
) -> Option<()> {
let sectors = ext_len.div_ceil(2048);
for i in 0..sectors {
let mut sector = [0u8; 2048];
seek_read(reader, byte_pos + i as u64 * 2048, &mut sector)?;
let already = data.len() as u64;
let remaining = total_len.saturating_sub(already) as usize;
let sector_bytes = (ext_len - i * 2048).min(2048);
let take = sector_bytes.min(remaining);
data.extend_from_slice(§or[..take]);
}
Some(())
}
fn decode_osta_cs0(bytes: &[u8]) -> String {
if bytes.is_empty() {
return String::new();
}
let comp_id = bytes[0];
let payload = &bytes[1..];
match comp_id {
8 => String::from_utf8_lossy(payload).into_owned(),
16 => {
let pairs: Vec<u16> = payload
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&pairs)
}
_ => String::from_utf8_lossy(payload).into_owned(),
}
}
fn seek_read<R: Read + Seek>(reader: &mut R, byte_pos: u64, buf: &mut [u8]) -> Option<()> {
reader.seek(SeekFrom::Start(byte_pos)).ok()?;
reader.read_exact(buf).ok()?;
Some(())
}