isobemak 0.2.4

Create bootable ISO images with FAT32 and UEFI (El Torito) support in Rust.
Documentation
use std::io::{self};
use std::path::Path;

use crate::iso::boot_catalog::{BOOT_CATALOG_EFI_PLATFORM_ID, BootCatalogEntry};
use crate::iso::fs_node::{IsoDirectory, IsoFsNode};
use crate::utils::ISO_SECTOR_SIZE;
const EL_TORITO_SECTOR_SIZE: u64 = 512;
/// Enum representing different boot types for cleaner boot entry creation
#[derive(Clone, Copy)]
pub enum BootType {
    Bios,
    Uefi,
    UefiEsp,
}

impl BootType {
    fn platform_id(self) -> u8 {
        match self {
            BootType::Bios => 0x00,
            BootType::Uefi | BootType::UefiEsp => BOOT_CATALOG_EFI_PLATFORM_ID,
        }
    }

    fn description(self) -> &'static str {
        match self {
            BootType::Bios => "BIOS boot",
            BootType::Uefi => "UEFI boot",
            BootType::UefiEsp => "UEFI ESP",
        }
    }
}

/// Calculates the Logical Block Addresses (LBAs) for all files and directories.
pub fn calculate_lbas(current_lba: &mut u32, dir: &mut IsoDirectory) -> io::Result<()> {
    dir.lba = *current_lba;
    *current_lba += 1;

    let mut sorted_children: Vec<_> = dir.children.iter_mut().collect();
    sorted_children.sort_by_key(|(name, _)| *name);

    for (_, node) in sorted_children {
        match node {
            IsoFsNode::File(file) => {
                file.lba = *current_lba;
                let sectors = file.size.div_ceil(ISO_SECTOR_SIZE as u64) as u32;
                *current_lba += sectors;
            }
            IsoFsNode::Directory(subdir) => {
                calculate_lbas(current_lba, subdir)?;
            }
        }
    }
    Ok(())
}

/// Helper to find the LBA for a given path in the ISO filesystem.
pub fn get_lba_for_path(root: &IsoDirectory, path: &str) -> io::Result<u32> {
    match get_node_for_path(root, path)? {
        IsoFsNode::File(file) => Ok(file.lba),
        IsoFsNode::Directory(_) => Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("Path is a directory, not a file: {}", path),
        )),
    }
}

/// Helper to find the size for a given path in the ISO filesystem.
pub fn get_file_size_in_iso(root: &IsoDirectory, path: &str) -> io::Result<u64> {
    match get_node_for_path(root, path)? {
        IsoFsNode::File(file) => Ok(file.size),
        IsoFsNode::Directory(_) => Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("Path is a directory, not a file: {}", path),
        )),
    }
}

/// Context for path operations, avoiding repeated parsing
struct PathContext<'a> {
    components: Vec<std::path::Component<'a>>,
    path_str: &'a str,
}

impl<'a> PathContext<'a> {
    fn new(path: &'a str) -> Self {
        Self {
            components: Path::new(path).components().collect(),
            path_str: path,
        }
    }

    fn validate_all(&self) -> io::Result<()> {
        validate_path_components(Path::new(self.path_str))
    }
}

/// Helper to find the IsoFsNode for a given path in the ISO filesystem.
fn get_node_for_path<'a>(root: &'a IsoDirectory, path: &str) -> io::Result<&'a IsoFsNode> {
    let path_ctx = PathContext::new(path);
    path_ctx.validate_all()?;

    let mut current_node = root;

    for (i, component) in path_ctx.components.iter().enumerate() {
        let component_name = ensure_path_component!(component, path_ctx.path_str);

        if i == path_ctx.components.len() - 1 {
            // Last component, this is the target node
            return current_node
                .children
                .get(component_name)
                .ok_or_else(|| io_error!(io::ErrorKind::NotFound, "Path not found: {}", path));
        } else {
            // Intermediate component, must be a directory
            match current_node.children.get(component_name) {
                Some(IsoFsNode::Directory(dir)) => current_node = dir,
                _ => {
                    return Err(io_error!(
                        io::ErrorKind::NotFound,
                        "Directory not found in path: {}",
                        path
                    ));
                }
            }
        }
    }
    // This part should be unreachable if components is not empty
    Err(io_error!(
        io::ErrorKind::NotFound,
        "Path not found: {}",
        path
    ))
}

/// Calculate the number of sectors needed for a given file size
pub fn calculate_sectors_from_size(file_size: u64) -> u32 {
    file_size.div_ceil(ISO_SECTOR_SIZE as u64) as u32
}

/// Validate that a boot image size is suitable for the boot catalog
pub fn validate_boot_image_size(size: u64, max_size: u64, image_type: &str) -> io::Result<()> {
    if size > max_size {
        return Err(io_error!(
            io::ErrorKind::InvalidInput,
            "{} image is too large for the boot catalog ({} > {})",
            image_type,
            size,
            max_size
        ));
    }
    Ok(())
}

/// Generic function to create any boot entry type (returns result directly)
pub fn create_boot_entry_generic(
    boot_type: BootType,
    root: &IsoDirectory,
    destination_path: Option<&str>,
    esp_lba: Option<u32>,
    esp_size_sectors: Option<u32>,
) -> io::Result<BootCatalogEntry> {
    Ok(BootCatalogEntry {
        platform_id: boot_type.platform_id(),
        boot_image_lba: match boot_type {
            BootType::Bios | BootType::Uefi => {
                let path = destination_path.ok_or_else(|| {
                    io_error!(
                        io::ErrorKind::InvalidInput,
                        "Path required for {} boot",
                        boot_type.description()
                    )
                })?;
                get_lba_for_path(root, path)?
            }
            BootType::UefiEsp => esp_lba.ok_or_else(|| {
                io_error!(
                    io::ErrorKind::InvalidInput,
                    "ESP LBA required for UEFI ESP boot"
                )
            })?,
        },
        boot_image_sectors: match boot_type {
            BootType::Bios | BootType::Uefi => {
                let path = destination_path.ok_or_else(|| {
                    io_error!(
                        io::ErrorKind::InvalidInput,
                        "Path required for {} boot",
                        boot_type.description()
                    )
                })?;
                let size = get_file_size_in_iso(root, path)?;
                let sectors = size.div_ceil(EL_TORITO_SECTOR_SIZE).max(1);
                validate_boot_image_size(sectors, u16::MAX as u64, boot_type.description())?;
                sectors as u16
            }
            BootType::UefiEsp => {
                let sectors = esp_size_sectors.ok_or_else(|| {
                    io_error!(
                        io::ErrorKind::InvalidInput,
                        "ESP size required for UEFI ESP boot"
                    )
                })?;
                let boot_image_512_sectors = sectors.checked_mul(4).ok_or_else(|| {
                    io_error!(
                        io::ErrorKind::InvalidInput,
                        "UEFI ESP boot image size calculation overflowed"
                    )
                })?;
                validate_boot_image_size(
                    boot_image_512_sectors as u64,
                    u16::MAX as u64,
                    boot_type.description(),
                )?;
                boot_image_512_sectors as u16
            }
        },
        bootable: true,
    })
}

/// Create a boot catalog entry for BIOS boot
pub fn create_bios_boot_entry(
    root: &IsoDirectory,
    destination_path: &str,
) -> io::Result<BootCatalogEntry> {
    create_boot_entry_generic(BootType::Bios, root, Some(destination_path), None, None)
}

/// Create a boot catalog entry for UEFI boot
pub fn create_uefi_boot_entry(
    root: &IsoDirectory,
    destination_path: &str,
) -> io::Result<BootCatalogEntry> {
    create_boot_entry_generic(BootType::Uefi, root, Some(destination_path), None, None)
}

/// Create a boot catalog entry for UEFI ESP partition
pub fn create_uefi_esp_boot_entry(
    esp_lba: u32,
    esp_size_sectors: u32,
) -> io::Result<BootCatalogEntry> {
    create_boot_entry_generic(
        BootType::UefiEsp,
        &IsoDirectory::new(),
        None,
        Some(esp_lba),
        Some(esp_size_sectors),
    )
}

/// Get file metadata with consistent error handling
pub fn get_file_metadata(path: &Path) -> io::Result<std::fs::Metadata> {
    std::fs::metadata(path).map_err(|e| {
        io_error!(
            io::ErrorKind::NotFound,
            "Failed to get file metadata for {}: {}",
            path.display(),
            e
        )
    })
}

/// Navigate to a directory by path, creating intermediate directories if they don't exist.
pub fn ensure_directory_path<'a>(
    root: &'a mut IsoDirectory,
    path: &str,
) -> io::Result<&'a mut IsoDirectory> {
    let components: Vec<_> = Path::new(path).components().collect();
    let mut current_dir = root;

    for component in components.iter().take(components.len().saturating_sub(1)) {
        let component_name = component
            .as_os_str()
            .to_str()
            .ok_or_else(|| io_error!(io::ErrorKind::InvalidInput, "Invalid path component"))?;
        current_dir = match current_dir
            .children
            .entry(component_name.to_string())
            .or_insert_with(|| IsoFsNode::Directory(IsoDirectory::new()))
        {
            IsoFsNode::Directory(dir) => dir,
            IsoFsNode::File(_) => {
                return Err(io_error!(
                    io::ErrorKind::AlreadyExists,
                    "Path component '{}' is a file, expected directory",
                    component_name
                ));
            }
        };
    }

    Ok(current_dir)
}

/// Validate path components with consistent error handling
pub fn validate_path_components(path: &Path) -> io::Result<()> {
    for component in path.components() {
        component.as_os_str().to_str().ok_or_else(|| {
            io_error!(
                io::ErrorKind::InvalidInput,
                "Invalid path component: {:?}",
                component
            )
        })?;
    }
    Ok(())
}