use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use lamfold::{
checked_full_read_len, BlockSource, DirEntry, FileKind, FoldError, FoldFrontend, Metadata,
NodeId, Result, SubstrateCtx,
};
use crate::{el_torito, rock_ridge};
const VD_REGION_OFFSET: u64 = 16 * 2048;
const VD_SIZE: usize = 2048;
const VD_TYPE_BOOT_RECORD: u8 = 0;
const VD_TYPE_PRIMARY: u8 = 1;
const VD_TYPE_SUPPLEMENTARY: u8 = 2;
const VD_TYPE_TERMINATOR: u8 = 255;
const STANDARD_ID: &[u8; 5] = b"CD001";
const JOLIET_ESCAPE_PREFIX: [u8; 2] = [0x25, 0x2F]; const JOLIET_ESCAPE_OFFSET: usize = 88;
const EL_TORITO_ID: &[u8] = b"EL TORITO SPECIFICATION";
const ET_CATALOG_PTR_OFFSET: usize = 71;
#[cfg(feature = "zisofs")]
const ZISOFS_MAGIC: [u8; 8] = [0x37, 0xE4, 0x53, 0x96, 0xC9, 0xDB, 0xD6, 0x07];
const MAX_VD_SCAN: u64 = 64;
mod dr {
pub const EXTENT_LBA_LE: usize = 2; pub const DATA_LEN_LE: usize = 10; pub const FILE_FLAGS: usize = 25;
pub const LEN_FI: usize = 32;
pub const FILE_ID: usize = 33;
pub const FLAG_DIRECTORY: u8 = 0x02;
}
#[derive(Clone)]
struct Inode {
lba: u32,
size: u32,
kind: FileKind,
link_target: Option<Vec<u8>>,
zisofs: Option<rock_ridge::Zisofs>,
}
pub struct Iso9660<S: BlockSource> {
src: S,
block_size: u32,
nodes: Vec<Inode>,
by_lba: BTreeMap<u32, NodeId>,
rock_ridge: bool,
susp_skip: usize,
joliet: bool,
el_torito_catalog_lba: Option<u32>,
}
impl<S: BlockSource> Iso9660<S> {
fn intern(&mut self, inode: Inode) -> NodeId {
if let Some(&id) = self.by_lba.get(&inode.lba) {
return id;
}
let lba = inode.lba;
let id = self.nodes.len() as NodeId;
self.nodes.push(inode);
self.by_lba.insert(lba, id);
id
}
fn inode(&self, node: NodeId) -> Result<Inode> {
self.nodes
.get(node as usize)
.cloned()
.ok_or(FoldError::NotFound)
}
fn read_dir_extent(&mut self, inode: Inode) -> Result<Vec<u8>> {
let len = checked_full_read_len(u64::from(inode.size))?;
let mut buf = vec![0u8; len];
let off = u64::from(inode.lba) * u64::from(self.block_size);
self.src.read_at(off, &mut buf)?;
Ok(buf)
}
fn detect_susp(&mut self, root: &Inode) -> Result<Option<usize>> {
let buf = self.read_dir_extent(root.clone())?;
if buf.is_empty() {
return Ok(None);
}
let len = buf[0] as usize;
let rec = buf
.get(..len)
.ok_or(FoldError::Corrupt("iso: short root record"))?;
let fi_len = *rec
.get(dr::LEN_FI)
.ok_or(FoldError::Corrupt("iso: short root record"))? as usize;
let pad = usize::from(fi_len.is_multiple_of(2));
let su_start = dr::FILE_ID + fi_len + pad;
let su = rec.get(su_start..).unwrap_or(&[]);
Ok(rock_ridge::detect_sp(su))
}
fn for_each_entry(&mut self, dir: Inode, mut f: impl FnMut(String, Inode)) -> Result<()> {
let buf = self.read_dir_extent(dir)?;
let bs = self.block_size as usize;
let mut pos = 0usize;
while pos < buf.len() {
let len = buf[pos] as usize;
if len == 0 {
let next = (pos / bs + 1) * bs;
if next <= pos {
break;
}
pos = next;
continue;
}
let rec = buf
.get(pos..pos + len)
.ok_or(FoldError::Corrupt("iso: directory record overruns extent"))?;
let fi_len = *rec
.get(dr::LEN_FI)
.ok_or(FoldError::Corrupt("iso: short record"))? as usize;
let fi = rec
.get(dr::FILE_ID..dr::FILE_ID + fi_len)
.ok_or(FoldError::Corrupt("iso: file id overruns record"))?;
let is_dot = fi_len == 1 && (fi[0] == 0x00 || fi[0] == 0x01);
if !is_dot {
let flags = *rec
.get(dr::FILE_FLAGS)
.ok_or(FoldError::Corrupt("iso: short flags"))?;
let is_dir = flags & dr::FLAG_DIRECTORY != 0;
let lba = le_u32(rec, dr::EXTENT_LBA_LE)?;
let size = le_u32(rec, dr::DATA_LEN_LE)?;
let mut name = if self.joliet {
decode_name_joliet(fi, is_dir)?
} else {
decode_name(fi, is_dir)?
};
let mut kind = if is_dir {
FileKind::Directory
} else {
FileKind::Regular
};
let mut link_target = None;
let mut zisofs = None;
if self.rock_ridge {
let pad = usize::from(fi_len.is_multiple_of(2));
let su_start = dr::FILE_ID + fi_len + pad;
if let Some(su) = rec.get(su_start..) {
let su = su.get(self.susp_skip..).unwrap_or(&[]);
let rr = rock_ridge::parse_su(su);
if let Some(n) = rr.name {
name = n;
}
if let Some(target) = rr.symlink_target {
kind = FileKind::Symlink;
link_target = Some(target);
}
zisofs = rr.zisofs;
}
}
f(
name,
Inode {
lba,
size,
kind,
link_target,
zisofs,
},
);
}
pos += len;
}
Ok(())
}
pub fn el_torito_uefi_image(&mut self) -> Result<Option<el_torito::UefiImage>> {
let Some(cat_lba) = self.el_torito_catalog_lba else {
return Ok(None);
};
let off = u64::from(cat_lba) * u64::from(self.block_size);
if off + VD_SIZE as u64 > self.src.len() {
return Ok(None);
}
let mut cat = vec![0u8; VD_SIZE];
self.src.read_at(off, &mut cat)?;
Ok(el_torito::parse_catalog(&cat))
}
#[cfg(feature = "zisofs")]
fn read_zisofs(
&mut self,
inode: &Inode,
zi: rock_ridge::Zisofs,
off: u64,
buf: &mut [u8],
) -> Result<usize> {
use lamfold::{decode, Codec};
let uncompressed = u64::from(zi.uncompressed_size);
if off >= uncompressed {
return Ok(0);
}
if zi.block_size_log2 >= 31 {
return Err(FoldError::Corrupt("zisofs: implausible block size"));
}
let block_size = 1u64 << zi.block_size_log2;
let n_blocks = uncompressed.div_ceil(block_size) as usize;
let table_bytes = (n_blocks + 1)
.checked_mul(4)
.ok_or(FoldError::Corrupt("zisofs: block table overflow"))?;
let head_len = checked_full_read_len(16u64 + table_bytes as u64)?;
let extent_off = u64::from(inode.lba) * u64::from(self.block_size);
let mut head = vec![0u8; head_len];
self.src.read_at(extent_off, &mut head)?;
if head[..8] != ZISOFS_MAGIC {
return Err(FoldError::Corrupt("zisofs: bad magic"));
}
let ptr = |i: usize| -> u32 {
let o = 16 + i * 4;
u32::from_le_bytes([head[o], head[o + 1], head[o + 2], head[o + 3]])
};
let want = core::cmp::min(buf.len() as u64, uncompressed - off) as usize;
let mut produced = 0;
let mut cur = off;
while produced < want {
let blk = (cur / block_size) as usize;
let blk_base = blk as u64 * block_size;
let this_uncomp = core::cmp::min(block_size, uncompressed - blk_base) as usize;
let cstart = u64::from(ptr(blk));
let cend = u64::from(ptr(blk + 1));
let decompressed: Vec<u8> = if cend <= cstart {
vec![0u8; this_uncomp] } else {
let clen = checked_full_read_len(cend - cstart)?;
let mut cbuf = vec![0u8; clen];
self.src.read_at(extent_off + cstart, &mut cbuf)?;
decode(Codec::Zlib, &cbuf, this_uncomp)?
};
let in_blk = (cur - blk_base) as usize;
let avail = decompressed.len().saturating_sub(in_blk);
let take = core::cmp::min(avail, want - produced);
if take == 0 {
break;
}
buf[produced..produced + take].copy_from_slice(&decompressed[in_blk..in_blk + take]);
produced += take;
cur += take as u64;
}
Ok(produced)
}
}
impl<S: BlockSource> FoldFrontend<S> for Iso9660<S> {
const TAG: &'static str = "iso9660";
fn probe(src: &mut S) -> Result<bool> {
let mut hdr = [0u8; 8];
if src.len() < VD_REGION_OFFSET + hdr.len() as u64 {
return Ok(false);
}
src.read_at(VD_REGION_OFFSET, &mut hdr)?;
Ok(&hdr[1..6] == STANDARD_ID)
}
fn open(src: S, _cx: &mut SubstrateCtx<'_>) -> Result<Self> {
let mut me = Iso9660 {
src,
block_size: 2048,
nodes: Vec::new(),
by_lba: BTreeMap::new(),
rock_ridge: false,
susp_skip: 0,
joliet: false,
el_torito_catalog_lba: None,
};
let mut vd = [0u8; VD_SIZE];
let mut pvd_root: Option<Inode> = None;
let mut joliet_root: Option<Inode> = None;
for i in 0..MAX_VD_SCAN {
let off = VD_REGION_OFFSET + i * VD_SIZE as u64;
if off + VD_SIZE as u64 > me.src.len() {
break;
}
me.src.read_at(off, &mut vd)?;
if &vd[1..6] != STANDARD_ID {
return Err(FoldError::Corrupt("iso: missing CD001 standard id"));
}
match vd[0] {
VD_TYPE_PRIMARY => {
me.block_size = u32::from(le_u16(&vd, 128)?);
if me.block_size == 0 {
return Err(FoldError::Corrupt("iso: zero logical block size"));
}
pvd_root = Some(root_record(&vd)?);
}
VD_TYPE_BOOT_RECORD => {
if vd.get(7..7 + EL_TORITO_ID.len()) == Some(EL_TORITO_ID) {
me.el_torito_catalog_lba = Some(le_u32(&vd, ET_CATALOG_PTR_OFFSET)?);
}
}
VD_TYPE_SUPPLEMENTARY => {
let esc = &vd[JOLIET_ESCAPE_OFFSET..JOLIET_ESCAPE_OFFSET + 3];
if esc[..2] == JOLIET_ESCAPE_PREFIX && matches!(esc[2], 0x40 | 0x43 | 0x45) {
joliet_root = Some(root_record(&vd)?);
}
}
VD_TYPE_TERMINATOR => break,
_ => continue,
}
}
let pvd_root = pvd_root.ok_or(FoldError::Corrupt("iso: no primary volume descriptor"))?;
let root = if let Some(skip) = me.detect_susp(&pvd_root)? {
me.rock_ridge = true;
me.susp_skip = skip;
pvd_root
} else if let Some(jr) = joliet_root {
me.joliet = true;
jr
} else {
pvd_root
};
me.intern(root); Ok(me)
}
fn root(&self) -> NodeId {
0
}
fn lookup(
&mut self,
dir: NodeId,
name: &str,
_cx: &mut SubstrateCtx<'_>,
) -> Result<Option<NodeId>> {
let dir_inode = self.inode(dir)?;
if dir_inode.kind != FileKind::Directory {
return Err(FoldError::NotDirectory);
}
let mut found: Option<Inode> = None;
self.for_each_entry(dir_inode, |n, inode| {
if found.is_none() && n == name {
found = Some(inode);
}
})?;
Ok(found.map(|i| self.intern(i)))
}
fn read_dir(&mut self, dir: NodeId, _cx: &mut SubstrateCtx<'_>) -> Result<Vec<DirEntry>> {
let dir_inode = self.inode(dir)?;
if dir_inode.kind != FileKind::Directory {
return Err(FoldError::NotDirectory);
}
let mut collected: Vec<(String, Inode)> = Vec::new();
self.for_each_entry(dir_inode, |n, inode| collected.push((n, inode)))?;
let mut out = Vec::with_capacity(collected.len());
for (name, inode) in collected {
let kind = inode.kind;
let node = self.intern(inode);
out.push(DirEntry { name, node, kind });
}
Ok(out)
}
fn metadata(&mut self, node: NodeId, _cx: &mut SubstrateCtx<'_>) -> Result<Metadata> {
let inode = self.inode(node)?;
let size = inode
.zisofs
.map_or(u64::from(inode.size), |z| u64::from(z.uncompressed_size));
Ok(Metadata {
kind: inode.kind,
size,
mode: 0,
})
}
fn read_at(
&mut self,
node: NodeId,
off: u64,
buf: &mut [u8],
_cx: &mut SubstrateCtx<'_>,
) -> Result<usize> {
let inode = self.inode(node)?;
if inode.kind == FileKind::Directory {
return Err(FoldError::IsDirectory);
}
#[cfg(feature = "zisofs")]
if let Some(zi) = inode.zisofs {
return self.read_zisofs(&inode, zi, off, buf);
}
#[cfg(not(feature = "zisofs"))]
if inode.zisofs.is_some() {
return Err(FoldError::Unsupported(
"zisofs-compressed file: enable the `zisofs` feature",
));
}
let size = u64::from(inode.size);
if off >= size {
return Ok(0);
}
let n = core::cmp::min(buf.len() as u64, size - off) as usize;
let at = u64::from(inode.lba) * u64::from(self.block_size) + off;
self.src.read_at(at, &mut buf[..n])?;
Ok(n)
}
fn read_link(&mut self, node: NodeId, _cx: &mut SubstrateCtx<'_>) -> Result<Option<Vec<u8>>> {
Ok(self.inode(node)?.link_target)
}
}
fn root_record(vd: &[u8]) -> Result<Inode> {
let rdr = vd
.get(156..156 + 34)
.ok_or(FoldError::Corrupt("iso: short root directory record"))?;
Ok(Inode {
lba: le_u32(rdr, dr::EXTENT_LBA_LE)?,
size: le_u32(rdr, dr::DATA_LEN_LE)?,
kind: FileKind::Directory,
link_target: None,
zisofs: None,
})
}
fn decode_name_joliet(fi: &[u8], is_dir: bool) -> Result<String> {
let units = fi.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]]));
let mut s = String::new();
for ch in char::decode_utf16(units) {
s.push(ch.map_err(|_| FoldError::InvalidPath("iso: bad UTF-16 in joliet name"))?);
}
if is_dir {
Ok(s)
} else {
let base = s.split(';').next().unwrap_or(&s);
let base = base.strip_suffix('.').unwrap_or(base);
Ok(String::from(base))
}
}
fn le_u16(b: &[u8], off: usize) -> Result<u16> {
b.get(off..off + 2)
.and_then(|s| s.try_into().ok())
.map(u16::from_le_bytes)
.ok_or(FoldError::Corrupt("iso: truncated u16 field"))
}
fn le_u32(b: &[u8], off: usize) -> Result<u32> {
b.get(off..off + 4)
.and_then(|s| s.try_into().ok())
.map(u32::from_le_bytes)
.ok_or(FoldError::Corrupt("iso: truncated u32 field"))
}
fn decode_name(fi: &[u8], is_dir: bool) -> Result<String> {
let raw =
core::str::from_utf8(fi).map_err(|_| FoldError::InvalidPath("iso: non-utf8 file id"))?;
let name = if is_dir {
raw
} else {
let base = raw.split(';').next().unwrap_or(raw);
base.strip_suffix('.').unwrap_or(base)
};
Ok(String::from(name))
}