use std::collections::BTreeMap;
use std::io::Read;
use std::path::{Path, PathBuf};
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::{DeviceKind, FileMeta, FileSource};
use super::el_torito::BootCatalog;
use super::joliet::string_to_ucs2_be;
const SECTOR: u64 = 2048;
const PVD_BYTE: u64 = 16 * SECTOR;
#[derive(Debug, Clone)]
pub struct FormatOpts {
pub volume_id: String,
pub publisher_id: String,
pub data_preparer_id: String,
pub application_id: String,
pub joliet: bool,
pub rock_ridge: bool,
pub el_torito: Option<BootCatalog>,
pub create_date: u32,
}
impl Default for FormatOpts {
fn default() -> Self {
Self {
volume_id: "CDROM".into(),
publisher_id: String::new(),
data_preparer_id: String::new(),
application_id: String::new(),
joliet: true,
rock_ridge: true,
el_torito: None,
create_date: 0,
}
}
}
enum PendingEntry {
File {
#[allow(dead_code)]
meta: FileMeta,
body: Vec<u8>,
},
Dir {
#[allow(dead_code)]
meta: FileMeta,
},
Symlink {
#[allow(dead_code)]
meta: FileMeta,
target: PathBuf,
},
Device {
#[allow(dead_code)]
meta: FileMeta,
#[allow(dead_code)]
kind: DeviceKind,
#[allow(dead_code)]
major: u32,
#[allow(dead_code)]
minor: u32,
},
}
pub struct Iso9660Writer {
opts: FormatOpts,
entries: BTreeMap<PathBuf, PendingEntry>,
flushed: bool,
}
impl Iso9660Writer {
pub fn new(opts: FormatOpts) -> Self {
Self {
opts,
entries: BTreeMap::new(),
flushed: false,
}
}
pub fn add_file(&mut self, path: &Path, src: FileSource, meta: FileMeta) -> Result<()> {
let path = normalize(path)?;
let (mut reader, total) = src.open()?;
let mut body = Vec::with_capacity(total as usize);
reader.read_to_end(&mut body)?;
self.entries.insert(path, PendingEntry::File { meta, body });
Ok(())
}
pub fn add_dir(&mut self, path: &Path, meta: FileMeta) -> Result<()> {
let path = normalize(path)?;
self.entries.insert(path, PendingEntry::Dir { meta });
Ok(())
}
pub fn add_symlink(&mut self, path: &Path, target: &Path, meta: FileMeta) -> Result<()> {
let path = normalize(path)?;
self.entries.insert(
path,
PendingEntry::Symlink {
meta,
target: target.to_path_buf(),
},
);
Ok(())
}
pub fn add_device(
&mut self,
path: &Path,
kind: DeviceKind,
major: u32,
minor: u32,
meta: FileMeta,
) -> Result<()> {
let path = normalize(path)?;
self.entries.insert(
path,
PendingEntry::Device {
meta,
kind,
major,
minor,
},
);
Ok(())
}
pub fn remove_entry(&mut self, path: &Path) -> Result<()> {
let path = normalize(path)?;
if self.entries.remove(&path).is_none() {
return Err(crate::Error::InvalidArgument(format!(
"iso9660: no buffered entry at {}",
path.display()
)));
}
Ok(())
}
pub fn entry_count(&self) -> usize {
self.entries.len()
}
pub fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
if self.flushed {
return Ok(());
}
let tree = build_tree(&self.entries)?;
let layout = compute_layout(&tree, &self.opts);
write_image(dev, &tree, &mut self.entries, &layout, &self.opts)?;
self.flushed = true;
Ok(())
}
}
struct Node {
path: PathBuf,
name: String,
kind: NodeKind,
children: Vec<Node>,
}
enum NodeKind {
Dir,
File { size: u64 },
Symlink { target: PathBuf },
Device,
}
fn build_tree(entries: &BTreeMap<PathBuf, PendingEntry>) -> Result<Node> {
let mut root = Node {
path: PathBuf::from("/"),
name: "/".to_string(),
kind: NodeKind::Dir,
children: Vec::new(),
};
for (path, entry) in entries {
let kind = match entry {
PendingEntry::Dir { .. } => NodeKind::Dir,
PendingEntry::File { body, .. } => NodeKind::File {
size: body.len() as u64,
},
PendingEntry::Symlink { target, .. } => NodeKind::Symlink {
target: target.clone(),
},
PendingEntry::Device { .. } => NodeKind::Device,
};
insert_node(&mut root, path, kind)?;
}
sort_tree(&mut root);
Ok(root)
}
fn insert_node(root: &mut Node, path: &Path, kind: NodeKind) -> Result<()> {
let components: Vec<&str> = path
.to_str()
.unwrap()
.trim_matches('/')
.split('/')
.filter(|c| !c.is_empty())
.collect();
if components.is_empty() {
return Err(crate::Error::InvalidArgument(
"iso9660: cannot insert root explicitly".into(),
));
}
let mut cur = root;
for (i, comp) in components.iter().enumerate() {
let is_leaf = i + 1 == components.len();
let mut full = cur.path.clone();
full.push(comp);
if let Some(idx) = cur.children.iter().position(|c| c.name == *comp) {
if is_leaf {
cur.children[idx].kind = match &kind {
NodeKind::Dir => NodeKind::Dir,
NodeKind::File { size } => NodeKind::File { size: *size },
NodeKind::Symlink { target } => NodeKind::Symlink {
target: target.clone(),
},
NodeKind::Device => NodeKind::Device,
};
}
cur = &mut cur.children[idx];
} else {
let child_kind = if is_leaf {
match &kind {
NodeKind::Dir => NodeKind::Dir,
NodeKind::File { size } => NodeKind::File { size: *size },
NodeKind::Symlink { target } => NodeKind::Symlink {
target: target.clone(),
},
NodeKind::Device => NodeKind::Device,
}
} else {
NodeKind::Dir
};
cur.children.push(Node {
path: full,
name: (*comp).to_string(),
kind: child_kind,
children: Vec::new(),
});
let n = cur.children.len() - 1;
cur = &mut cur.children[n];
}
}
Ok(())
}
fn sort_tree(node: &mut Node) {
node.children.sort_by(|a, b| a.name.cmp(&b.name));
for child in node.children.iter_mut() {
sort_tree(child);
}
}
struct Layout {
dir_lba: BTreeMap<PathBuf, (u32, u64)>,
joliet_dir_lba: BTreeMap<PathBuf, (u32, u64)>,
file_lba: BTreeMap<PathBuf, (u32, u64)>,
l_path_lba: u32,
m_path_lba: u32,
joliet_l_path_lba: u32,
joliet_m_path_lba: u32,
path_table_size: u32,
joliet_path_table_size: u32,
vdst_lba: u32,
total_sectors: u32,
}
fn compute_layout(root: &Node, opts: &FormatOpts) -> Layout {
let joliet = opts.joliet;
let mut cursor: u32 = 16; cursor += 1; if joliet {
cursor += 1; }
let vdst_lba = cursor;
cursor += 1;
let pvd_dirs = collect_directories(root);
let path_table_size = path_table_byte_size(&pvd_dirs, false);
let l_path_lba = cursor;
cursor += sectors_for(u64::from(path_table_size));
let m_path_lba = cursor;
cursor += sectors_for(u64::from(path_table_size));
let (joliet_l_path_lba, joliet_m_path_lba, joliet_path_table_size) = if joliet {
let size = path_table_byte_size(&pvd_dirs, true);
let l = cursor;
cursor += sectors_for(u64::from(size));
let m = cursor;
cursor += sectors_for(u64::from(size));
(l, m, size)
} else {
(0, 0, 0)
};
let mut dir_lba: BTreeMap<PathBuf, (u32, u64)> = BTreeMap::new();
for (path, _) in pvd_dirs.iter() {
let n = find_node(root, path).unwrap();
let size = dir_records_byte_size(n, opts, false);
dir_lba.insert(path.clone(), (cursor, size));
cursor += sectors_for(size);
}
let mut joliet_dir_lba: BTreeMap<PathBuf, (u32, u64)> = BTreeMap::new();
if joliet {
for (path, _) in pvd_dirs.iter() {
let n = find_node(root, path).unwrap();
let size = dir_records_byte_size(n, opts, true);
joliet_dir_lba.insert(path.clone(), (cursor, size));
cursor += sectors_for(size);
}
}
let mut file_lba: BTreeMap<PathBuf, (u32, u64)> = BTreeMap::new();
walk_files(root, &mut |n| {
if let NodeKind::File { size } = n.kind {
file_lba.insert(n.path.clone(), (cursor, size));
cursor += sectors_for(size.max(1));
}
});
Layout {
dir_lba,
joliet_dir_lba,
file_lba,
l_path_lba,
m_path_lba,
joliet_l_path_lba,
joliet_m_path_lba,
path_table_size,
joliet_path_table_size,
vdst_lba,
total_sectors: cursor,
}
}
fn collect_directories(root: &Node) -> Vec<(PathBuf, u16)> {
let mut out: Vec<(PathBuf, u16)> = vec![(root.path.clone(), 1)];
let mut queue: Vec<(usize, &Node)> = vec![(0, root)];
while let Some((parent_idx_minus1, parent)) = queue.pop() {
for child in &parent.children {
if matches!(child.kind, NodeKind::Dir) {
let parent_record = (parent_idx_minus1 + 1) as u16;
out.push((child.path.clone(), parent_record));
let new_idx = out.len() - 1;
queue.push((new_idx, child));
}
}
}
out
}
fn find_node<'a>(root: &'a Node, path: &Path) -> Option<&'a Node> {
if path == Path::new("/") {
return Some(root);
}
let comps: Vec<&str> = path
.to_str()?
.trim_matches('/')
.split('/')
.filter(|c| !c.is_empty())
.collect();
let mut cur = root;
for comp in comps {
cur = cur.children.iter().find(|c| c.name == comp)?;
}
Some(cur)
}
fn walk_files<F: FnMut(&Node)>(root: &Node, f: &mut F) {
f(root);
for c in &root.children {
walk_files(c, f);
}
}
fn sectors_for(bytes: u64) -> u32 {
bytes.div_ceil(SECTOR) as u32
}
fn path_table_byte_size(dirs: &[(PathBuf, u16)], joliet: bool) -> u32 {
let mut total: u32 = 0;
for (path, _parent) in dirs {
let name_bytes = if path == Path::new("/") {
1u32 } else {
let comp = path.file_name().unwrap().to_str().unwrap();
iso_name_bytes(comp, joliet, true) as u32
};
let pad = if name_bytes % 2 == 1 { 1 } else { 0 };
total += 8 + name_bytes + pad;
}
total
}
fn iso_name_bytes(name: &str, joliet: bool, directory: bool) -> usize {
if joliet {
name.encode_utf16().count() * 2
} else if directory {
name.to_ascii_uppercase().len().max(1)
} else {
name.to_ascii_uppercase().len() + 2
}
}
fn dir_records_byte_size(dir: &Node, opts: &FormatOpts, joliet: bool) -> u64 {
let mut sum: u64 = 0;
sum += 34 * 2;
let want_rr = opts.rock_ridge && !joliet;
for child in &dir.children {
sum += dir_record_size(child, want_rr, joliet) as u64;
}
align_records_to_sector(sum)
}
fn align_records_to_sector(bytes: u64) -> u64 {
bytes.div_ceil(SECTOR) * SECTOR
}
fn dir_record_size(node: &Node, rock_ridge: bool, joliet: bool) -> usize {
let name_bytes = iso_name_bytes(&node.name, joliet, matches!(node.kind, NodeKind::Dir));
let name_pad = if name_bytes % 2 == 0 { 1 } else { 0 };
let base = 33 + name_bytes + name_pad;
if rock_ridge {
let nm = 5 + node.name.len();
let px = 36;
let sl = if let NodeKind::Symlink { target } = &node.kind {
let mut s = 5;
for comp in target.to_str().unwrap_or("").split('/') {
if comp.is_empty() {
s += 2; } else {
s += 2 + comp.len();
}
}
s
} else {
0
};
base + nm + px + sl
} else {
base
}
}
fn write_image(
dev: &mut dyn BlockDevice,
root: &Node,
entries: &mut BTreeMap<PathBuf, PendingEntry>,
layout: &Layout,
opts: &FormatOpts,
) -> Result<()> {
let zero = vec![0u8; SECTOR as usize];
for s in 0u64..16 {
dev.write_at(s * SECTOR, &zero)?;
}
let pvd = encode_pvd(layout, opts, root, false);
dev.write_at(PVD_BYTE, &pvd)?;
if opts.joliet {
let svd = encode_pvd(layout, opts, root, true);
dev.write_at(17 * SECTOR, &svd)?;
}
let mut vdst = vec![0u8; SECTOR as usize];
vdst[0] = 0xFF;
vdst[1..6].copy_from_slice(b"CD001");
vdst[6] = 0x01;
dev.write_at(u64::from(layout.vdst_lba) * SECTOR, &vdst)?;
let dirs = collect_directories(root);
let (lpath, mpath) = encode_path_tables(&dirs, &layout.dir_lba, false);
dev.write_at(u64::from(layout.l_path_lba) * SECTOR, &lpath)?;
dev.write_at(u64::from(layout.m_path_lba) * SECTOR, &mpath)?;
if opts.joliet {
let (lpath, mpath) =
encode_path_tables(&dirs, &layout.joliet_dir_lba, true);
dev.write_at(u64::from(layout.joliet_l_path_lba) * SECTOR, &lpath)?;
dev.write_at(u64::from(layout.joliet_m_path_lba) * SECTOR, &mpath)?;
}
for (path, _) in dirs.iter() {
let (lba, _size) = layout.dir_lba.get(path).copied().unwrap();
let node = find_node(root, path).unwrap();
let parent_path = parent_of(path);
let parent_node = find_node(root, &parent_path).unwrap();
let parent_lba = layout.dir_lba.get(&parent_path).copied().unwrap().0;
let self_lba = lba;
let stream = encode_dir_records(
node,
parent_node,
self_lba,
parent_lba,
&layout.dir_lba,
&layout.file_lba,
opts,
false,
);
dev.write_at(u64::from(lba) * SECTOR, &stream)?;
}
if opts.joliet {
for (path, _) in dirs.iter() {
let (lba, _) = layout.joliet_dir_lba.get(path).copied().unwrap();
let node = find_node(root, path).unwrap();
let parent_path = parent_of(path);
let parent_node = find_node(root, &parent_path).unwrap();
let parent_lba = layout.joliet_dir_lba.get(&parent_path).copied().unwrap().0;
let stream = encode_dir_records(
node,
parent_node,
lba,
parent_lba,
&layout.joliet_dir_lba,
&layout.file_lba,
opts,
true,
);
dev.write_at(u64::from(lba) * SECTOR, &stream)?;
}
}
for (path, (lba, _size)) in layout.file_lba.iter() {
let Some(entry) = entries.get(path) else {
continue;
};
let PendingEntry::File { body, .. } = entry else {
continue;
};
let base = u64::from(*lba) * SECTOR;
let total = body.len() as u64;
if !body.is_empty() {
dev.write_at(base, body)?;
}
let used = total % SECTOR;
if used != 0 {
let pad = (SECTOR - used) as usize;
let z = vec![0u8; pad];
dev.write_at(base + total, &z)?;
}
}
Ok(())
}
fn parent_of(p: &Path) -> PathBuf {
if p == Path::new("/") {
return PathBuf::from("/");
}
p.parent()
.map(|x| x.to_path_buf())
.unwrap_or_else(|| PathBuf::from("/"))
}
fn encode_pvd(layout: &Layout, opts: &FormatOpts, root: &Node, joliet: bool) -> Vec<u8> {
let mut buf = vec![0u8; SECTOR as usize];
buf[0] = if joliet { 2 } else { 1 };
buf[1..6].copy_from_slice(b"CD001");
buf[6] = 1;
if joliet {
buf[88..91].copy_from_slice(b"%/E");
let sys = string_to_ucs2_be("");
write_padded_ucs2(&mut buf[8..40], &sys);
let vol = string_to_ucs2_be(&opts.volume_id);
write_padded_ucs2(&mut buf[40..72], &vol);
} else {
write_padded_ascii(&mut buf[8..40], "LINUX");
write_padded_ascii(&mut buf[40..72], &opts.volume_id);
}
put_both_u32(&mut buf[80..88], layout.total_sectors);
put_both_u16(&mut buf[128..132], 2048);
let pts = if joliet {
layout.joliet_path_table_size
} else {
layout.path_table_size
};
put_both_u32(&mut buf[132..140], pts);
let (lp, mp) = if joliet {
(layout.joliet_l_path_lba, layout.joliet_m_path_lba)
} else {
(layout.l_path_lba, layout.m_path_lba)
};
buf[140..144].copy_from_slice(&lp.to_le_bytes());
buf[148..152].copy_from_slice(&mp.to_be_bytes());
let root_dirs = if joliet {
&layout.joliet_dir_lba
} else {
&layout.dir_lba
};
let (root_lba, root_size) = root_dirs.get(Path::new("/")).copied().unwrap();
let root_rec = encode_root_dir_record(root_lba, root_size);
buf[156..156 + 34].copy_from_slice(&root_rec);
if joliet {
let app = string_to_ucs2_be(&opts.application_id);
let pub_ = string_to_ucs2_be(&opts.publisher_id);
let prep = string_to_ucs2_be(&opts.data_preparer_id);
write_padded_ucs2(&mut buf[190..318], &[]); write_padded_ucs2(&mut buf[318..446], &pub_);
write_padded_ucs2(&mut buf[446..574], &prep);
write_padded_ucs2(&mut buf[574..702], &app);
} else {
write_padded_ascii(&mut buf[190..318], ""); write_padded_ascii(&mut buf[318..446], &opts.publisher_id);
write_padded_ascii(&mut buf[446..574], &opts.data_preparer_id);
write_padded_ascii(&mut buf[574..702], &opts.application_id);
}
write_padded_ascii(&mut buf[702..739], "");
write_padded_ascii(&mut buf[739..776], "");
write_padded_ascii(&mut buf[776..813], "");
let date_bytes = encode_iso_long_date(opts.create_date);
buf[813..830].copy_from_slice(&date_bytes); buf[830..847].copy_from_slice(&date_bytes); buf[881] = 1;
let _ = root;
buf
}
fn encode_root_dir_record(lba: u32, size: u64) -> [u8; 34] {
let mut r = [0u8; 34];
r[0] = 34; r[1] = 0; put_both_u32(&mut r[2..10], lba);
put_both_u32(&mut r[10..18], size as u32);
r[25] = 0x02;
put_both_u16(&mut r[28..32], 1);
r[32] = 1; r[33] = 0x00; r
}
fn encode_iso_long_date(epoch: u32) -> [u8; 17] {
let mut d = [b'0'; 17];
d[16] = 0; if epoch == 0 {
return d;
}
let days = epoch / 86400;
let h = (epoch / 3600) % 24;
let m = (epoch / 60) % 60;
let s = epoch % 60;
let (y, mo, da) = days_to_ymd(i64::from(days) + 719468);
let yr = y as u32;
let s_year = format!("{yr:04}");
let s_month = format!("{mo:02}");
let s_day = format!("{da:02}");
let s_hour = format!("{h:02}");
let s_min = format!("{m:02}");
let s_sec = format!("{s:02}");
d[0..4].copy_from_slice(s_year.as_bytes());
d[4..6].copy_from_slice(s_month.as_bytes());
d[6..8].copy_from_slice(s_day.as_bytes());
d[8..10].copy_from_slice(s_hour.as_bytes());
d[10..12].copy_from_slice(s_min.as_bytes());
d[12..14].copy_from_slice(s_sec.as_bytes());
d[14..16].copy_from_slice(b"00"); d
}
fn days_to_ymd(z: i64) -> (i64, i64, i64) {
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
fn put_both_u16(buf: &mut [u8], v: u16) {
buf[0..2].copy_from_slice(&v.to_le_bytes());
buf[2..4].copy_from_slice(&v.to_be_bytes());
}
fn put_both_u32(buf: &mut [u8], v: u32) {
buf[0..4].copy_from_slice(&v.to_le_bytes());
buf[4..8].copy_from_slice(&v.to_be_bytes());
}
fn write_padded_ascii(buf: &mut [u8], s: &str) {
let bytes = s.as_bytes();
let n = bytes.len().min(buf.len());
buf[..n].copy_from_slice(&bytes[..n]);
for b in &mut buf[n..] {
*b = b' ';
}
}
fn write_padded_ucs2(buf: &mut [u8], ucs: &[u8]) {
let n = ucs.len().min(buf.len() / 2 * 2);
buf[..n].copy_from_slice(&ucs[..n]);
for chunk in buf[n..].chunks_exact_mut(2) {
chunk[0] = 0x00;
chunk[1] = 0x20;
}
}
fn encode_path_tables(
dirs: &[(PathBuf, u16)],
dir_lba: &BTreeMap<PathBuf, (u32, u64)>,
joliet: bool,
) -> (Vec<u8>, Vec<u8>) {
let mut lpath: Vec<u8> = Vec::new();
let mut mpath: Vec<u8> = Vec::new();
for (path, parent_idx) in dirs {
let (lba, _) = dir_lba.get(path).copied().unwrap();
let name_bytes = if path == Path::new("/") {
vec![0u8]
} else {
let comp = path.file_name().unwrap().to_str().unwrap();
iso_identifier_bytes(comp, joliet, true)
};
let len_di = name_bytes.len() as u8;
lpath.push(len_di);
lpath.push(0); lpath.extend_from_slice(&lba.to_le_bytes());
lpath.extend_from_slice(&parent_idx.to_le_bytes());
lpath.extend_from_slice(&name_bytes);
if name_bytes.len() % 2 == 1 {
lpath.push(0);
}
mpath.push(len_di);
mpath.push(0);
mpath.extend_from_slice(&lba.to_be_bytes());
mpath.extend_from_slice(&parent_idx.to_be_bytes());
mpath.extend_from_slice(&name_bytes);
if name_bytes.len() % 2 == 1 {
mpath.push(0);
}
}
(lpath, mpath)
}
fn iso_identifier_bytes(name: &str, joliet: bool, directory: bool) -> Vec<u8> {
if joliet {
return string_to_ucs2_be(name);
}
let mut s = name.to_ascii_uppercase();
if !directory {
s.push_str(";1");
}
s.into_bytes()
}
#[allow(clippy::too_many_arguments)]
fn encode_dir_records(
dir: &Node,
parent: &Node,
self_lba: u32,
parent_lba: u32,
dir_lba: &BTreeMap<PathBuf, (u32, u64)>,
file_lba: &BTreeMap<PathBuf, (u32, u64)>,
opts: &FormatOpts,
joliet: bool,
) -> Vec<u8> {
let mut buf: Vec<u8> = Vec::new();
let self_size = dir_lba.get(&dir.path).copied().unwrap().1;
buf.extend_from_slice(&encode_dot_record(self_lba, self_size, 0x00));
let parent_size = dir_lba.get(&parent.path).copied().unwrap().1;
buf.extend_from_slice(&encode_dot_record(parent_lba, parent_size, 0x01));
for child in &dir.children {
let rec = encode_child_record(child, dir_lba, file_lba, opts, joliet);
let sector_used = buf.len() as u64 % SECTOR;
if sector_used + rec.len() as u64 > SECTOR {
let pad = (SECTOR - sector_used) as usize;
buf.extend(std::iter::repeat_n(0u8, pad));
}
buf.extend_from_slice(&rec);
}
let used = buf.len() as u64 % SECTOR;
if used != 0 {
let pad = (SECTOR - used) as usize;
buf.extend(std::iter::repeat_n(0u8, pad));
}
buf
}
fn encode_dot_record(lba: u32, size: u64, ident: u8) -> [u8; 34] {
let mut r = [0u8; 34];
r[0] = 34;
put_both_u32(&mut r[2..10], lba);
put_both_u32(&mut r[10..18], size as u32);
r[25] = 0x02; put_both_u16(&mut r[28..32], 1);
r[32] = 1;
r[33] = ident;
r
}
fn encode_child_record(
child: &Node,
dir_lba: &BTreeMap<PathBuf, (u32, u64)>,
file_lba: &BTreeMap<PathBuf, (u32, u64)>,
opts: &FormatOpts,
joliet: bool,
) -> Vec<u8> {
let (lba, size) = match &child.kind {
NodeKind::Dir => dir_lba.get(&child.path).copied().unwrap_or((0, 0)),
NodeKind::File { size } => file_lba.get(&child.path).copied().unwrap_or((0, *size)),
_ => (0, 0),
};
let name_bytes = iso_identifier_bytes(&child.name, joliet, matches!(child.kind, NodeKind::Dir));
let len_fi = name_bytes.len() as u8;
let name_pad = if name_bytes.len() % 2 == 0 { 1 } else { 0 };
let want_rr = opts.rock_ridge && !joliet;
let mut sua: Vec<u8> = Vec::new();
if want_rr {
sua.extend_from_slice(b"NM");
let nm_len = 5 + child.name.len();
sua.push(nm_len as u8);
sua.push(1); sua.push(0); sua.extend_from_slice(child.name.as_bytes());
sua.extend_from_slice(b"PX");
sua.push(36); sua.push(1); let mode: u32 = match &child.kind {
NodeKind::Dir => 0o040755,
NodeKind::File { .. } => 0o100644,
NodeKind::Symlink { .. } => 0o120777,
NodeKind::Device => 0o020644,
};
let mut both = [0u8; 32];
put_both_u32(&mut both[0..8], mode);
put_both_u32(&mut both[8..16], 1); put_both_u32(&mut both[16..24], 0); put_both_u32(&mut both[24..32], 0); sua.extend_from_slice(&both);
if let NodeKind::Symlink { target } = &child.kind {
let mut comps_bytes: Vec<u8> = Vec::new();
for comp in target.to_str().unwrap_or("").split('/') {
if comp.is_empty() {
comps_bytes.push(0x08);
comps_bytes.push(0);
} else {
let bytes = comp.as_bytes();
comps_bytes.push(0x00);
comps_bytes.push(bytes.len() as u8);
comps_bytes.extend_from_slice(bytes);
}
}
sua.extend_from_slice(b"SL");
sua.push((5 + comps_bytes.len()) as u8);
sua.push(1); sua.push(0); sua.extend_from_slice(&comps_bytes);
}
}
let base = 33 + name_bytes.len() + name_pad;
let total = base + sua.len();
let mut rec = vec![0u8; total];
rec[0] = total as u8;
put_both_u32(&mut rec[2..10], lba);
put_both_u32(&mut rec[10..18], size as u32);
let flags = if matches!(child.kind, NodeKind::Dir) {
0x02
} else {
0x00
};
rec[25] = flags;
put_both_u16(&mut rec[28..32], 1); rec[32] = len_fi;
rec[33..33 + name_bytes.len()].copy_from_slice(&name_bytes);
let sua_start = base;
rec[sua_start..sua_start + sua.len()].copy_from_slice(&sua);
rec
}
fn normalize(path: &Path) -> Result<PathBuf> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("iso9660: non-UTF-8 path".into()))?;
let trimmed = s.trim_end_matches('/');
if trimmed.is_empty() || trimmed == "/" {
return Err(crate::Error::InvalidArgument(
"iso9660: cannot create root explicitly".into(),
));
}
if !trimmed.starts_with('/') {
return Err(crate::Error::InvalidArgument(format!(
"iso9660: path must be absolute: {trimmed}"
)));
}
Ok(PathBuf::from(trimmed))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
use crate::fs::FileMeta;
use std::io::Cursor;
#[test]
fn write_then_read_round_trip() {
let mut dev = MemoryBackend::new(8 * 1024 * 1024);
let opts = FormatOpts {
volume_id: "ROUNDTRIP".into(),
..FormatOpts::default()
};
let mut w = Iso9660Writer::new(opts);
w.add_dir(Path::new("/etc"), FileMeta::default()).unwrap();
let body = b"hello world\n".to_vec();
let src = FileSource::Reader {
reader: Box::new(Cursor::new(body.clone())),
len: body.len() as u64,
};
w.add_file(Path::new("/etc/conf"), src, FileMeta::default())
.unwrap();
w.flush(&mut dev).unwrap();
let iso = super::super::Iso9660::open(&mut dev).unwrap();
assert_eq!(iso.volume_id(), "ROUNDTRIP");
let root = iso.list_path(&mut dev, "/").unwrap();
let names: Vec<_> = root.iter().map(|d| d.name.clone()).collect();
assert!(names.iter().any(|n| n == "etc"));
let etc = iso.list_path(&mut dev, "/etc").unwrap();
assert!(etc.iter().any(|d| d.name == "conf"));
}
}