use crate::error::{Error, Result};
use crate::options::OpticalImageOptions;
use crate::tree::{Directory, FileExtent, FileTree};
#[derive(Debug)]
pub struct LayoutManager {
sector_size: usize,
next_file_sector: u32,
next_udf_block: u32,
next_unique_id: u64,
}
impl LayoutManager {
pub fn new(sector_size: usize) -> Self {
Self {
sector_size,
next_file_sector: 0,
next_udf_block: 0,
next_unique_id: 16, }
}
pub fn layout_files(
&mut self,
tree: &mut FileTree,
options: &OpticalImageOptions,
) -> Result<LayoutInfo> {
let vds_end = self.calculate_vds_end(options);
let udf_partition_start = 290;
let mut next_udf_block = 1;
let udf_root =
Self::plan_udf_directory(&tree.root, &mut next_udf_block, None, self.sector_size)?;
let udf_metadata_sectors = next_udf_block;
self.next_udf_block = udf_metadata_sectors;
self.next_file_sector = udf_partition_start + udf_metadata_sectors;
self.assign_file_extents(&mut tree.root)?;
self.assign_unique_ids(&mut tree.root);
let file_data_end = self.next_file_sector;
Ok(LayoutInfo {
vds_end,
udf_partition_start,
udf_metadata_sectors,
file_data_start: udf_partition_start + udf_metadata_sectors,
file_data_end,
total_sectors: file_data_end + 100, udf_root,
})
}
fn calculate_vds_end(&self, options: &OpticalImageOptions) -> u32 {
let mut sector = 16;
if options.iso.enabled {
sector += 1;
}
if options.iso.joliet.is_some() {
sector += 1;
}
if options.iso.long_filenames {
sector += 1;
}
if options.boot.is_some() {
sector += 1;
}
sector += 1;
sector
}
fn plan_udf_directory(
dir: &Directory,
next_block: &mut u32,
parent_icb: Option<u32>,
sector_size: usize,
) -> Result<UdfDirectoryLayout> {
let icb_block = *next_block;
*next_block = next_block
.checked_add(1)
.ok_or_else(|| Error::InvalidConfig("UDF metadata block overflow".into()))?;
let mut fid_bytes = 40usize; for name in dir
.files
.iter()
.map(|file| file.name.as_str())
.chain(dir.subdirs.iter().map(|child| child.name.as_str()))
{
let encoded_len = cs0_filename_len(name)?;
fid_bytes = fid_bytes
.checked_add((38 + encoded_len + 3) & !3)
.ok_or_else(|| Error::InvalidConfig("UDF FID size overflow".into()))?;
}
let fid_sectors = fid_bytes.div_ceil(sector_size) as u32;
let fid_block = *next_block;
*next_block = next_block
.checked_add(fid_sectors)
.ok_or_else(|| Error::InvalidConfig("UDF metadata block overflow".into()))?;
let mut file_icb_blocks = Vec::with_capacity(dir.files.len());
for _ in &dir.files {
file_icb_blocks.push(*next_block);
*next_block = next_block
.checked_add(1)
.ok_or_else(|| Error::InvalidConfig("UDF metadata block overflow".into()))?;
}
let mut subdirs = Vec::with_capacity(dir.subdirs.len());
for child in &dir.subdirs {
subdirs.push(Self::plan_udf_directory(
child,
next_block,
Some(icb_block),
sector_size,
)?);
}
Ok(UdfDirectoryLayout {
icb_block,
parent_icb_block: parent_icb.unwrap_or(icb_block),
fid_block,
fid_bytes,
file_icb_blocks,
subdirs,
})
}
fn assign_file_extents(&mut self, dir: &mut Directory) -> Result<()> {
for file in &mut dir.files {
let size = file
.size()
.map_err(|error| Error::Io(hadris_io::Error::from_source(error).erase()))?;
if size == 0 {
file.extent = FileExtent::new(0, 0);
} else {
file.extent = FileExtent::new(self.next_file_sector, size);
let sectors = file.extent.sector_count(self.sector_size);
self.next_file_sector += sectors;
}
}
for subdir in &mut dir.subdirs {
self.assign_file_extents(subdir)?;
}
Ok(())
}
fn assign_unique_ids(&mut self, dir: &mut Directory) {
dir.unique_id = self.next_unique_id;
self.next_unique_id += 1;
for file in &mut dir.files {
file.unique_id = self.next_unique_id;
self.next_unique_id += 1;
}
for subdir in &mut dir.subdirs {
self.assign_unique_ids(subdir);
}
}
pub fn allocate_udf_block(&mut self) -> u32 {
let block = self.next_udf_block;
self.next_udf_block += 1;
block
}
pub fn next_unique_id(&mut self) -> u64 {
let id = self.next_unique_id;
self.next_unique_id += 1;
id
}
}
#[derive(Debug, Clone)]
pub struct LayoutInfo {
pub vds_end: u32,
pub udf_partition_start: u32,
pub udf_metadata_sectors: u32,
pub file_data_start: u32,
pub file_data_end: u32,
pub total_sectors: u32,
pub(crate) udf_root: UdfDirectoryLayout,
}
#[derive(Debug, Clone)]
pub(crate) struct UdfDirectoryLayout {
pub(crate) icb_block: u32,
pub(crate) parent_icb_block: u32,
pub(crate) fid_block: u32,
pub(crate) fid_bytes: usize,
pub(crate) file_icb_blocks: Vec<u32>,
pub(crate) subdirs: Vec<UdfDirectoryLayout>,
}
fn cs0_filename_len(name: &str) -> Result<usize> {
let content_len = if name.chars().all(|ch| (ch as u32) <= 0xff) {
name.chars().count()
} else {
name.encode_utf16()
.count()
.checked_mul(2)
.ok_or_else(|| Error::InvalidConfig("UDF filename encoded length overflow".into()))?
};
let encoded_len = content_len + 1;
if encoded_len > u8::MAX as usize {
return Err(Error::InvalidPath(format!(
"UDF filename exceeds the 255-byte encoded limit: {name}"
)));
}
Ok(encoded_len)
}
impl core::fmt::Display for LayoutInfo {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"layout: {} total sectors (files at sectors {}-{})",
self.total_sectors, self.file_data_start, self.file_data_end
)
}
}
impl LayoutInfo {
pub fn udf_partition_length(&self) -> u32 {
self.total_sectors - self.udf_partition_start
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tree::FileEntry;
#[test]
fn test_layout_empty_tree() {
let mut tree = FileTree::new();
let options = OpticalImageOptions::default();
let mut layout = LayoutManager::new(2048);
let info = layout.layout_files(&mut tree, &options).unwrap();
assert!(info.file_data_end >= info.file_data_start);
}
#[test]
fn test_layout_with_files() {
let mut tree = FileTree::new();
tree.add_file(FileEntry::from_buffer("test.txt", vec![0u8; 4096]));
tree.add_file(FileEntry::from_buffer("small.txt", vec![0u8; 100]));
let options = OpticalImageOptions::default();
let mut layout = LayoutManager::new(2048);
layout.layout_files(&mut tree, &options).unwrap();
let file1 = tree.root.files.first().unwrap();
assert!(file1.extent.sector > 0);
assert_eq!(file1.extent.length, 4096);
let file2 = tree.root.files.get(1).unwrap();
assert!(file2.extent.sector > file1.extent.sector);
}
#[test]
fn test_layout_zero_size_file() {
let mut tree = FileTree::new();
tree.add_file(FileEntry::from_buffer("empty.txt", vec![]));
let options = OpticalImageOptions::default();
let mut layout = LayoutManager::new(2048);
layout.layout_files(&mut tree, &options).unwrap();
let file = tree.root.files.first().unwrap();
assert_eq!(file.extent.sector, 0);
assert_eq!(file.extent.length, 0);
}
}