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;
#[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",
}
}
}
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(())
}
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),
)),
}
}
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),
)),
}
}
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))
}
}
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 {
return current_node
.children
.get(component_name)
.ok_or_else(|| io_error!(io::ErrorKind::NotFound, "Path not found: {}", path));
} else {
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
));
}
}
}
}
Err(io_error!(
io::ErrorKind::NotFound,
"Path not found: {}",
path
))
}
pub fn calculate_sectors_from_size(file_size: u64) -> u32 {
file_size.div_ceil(ISO_SECTOR_SIZE as u64) as u32
}
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(())
}
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,
})
}
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)
}
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)
}
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),
)
}
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
)
})
}
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)
}
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(())
}