use std::fs::File;
use std::io::{self, Seek, SeekFrom}; use std::path::{Path, PathBuf};
use tempfile::NamedTempFile;
use uuid::Uuid;
use crate::fat;
use crate::iso::constants::ESP_START_LBA;
use crate::utils::ISO_SECTOR_SIZE;
use crate::iso::boot_catalog::BootCatalogEntry;
use crate::iso::boot_info::BootInfo;
use crate::iso::builder_utils::{
calculate_lbas, create_bios_boot_entry, create_uefi_boot_entry, create_uefi_esp_boot_entry,
ensure_directory_path, get_file_metadata,
};
use crate::iso::fs_node::{IsoDirectory, IsoFile, IsoFsNode};
use crate::iso::gpt::main_gpt_functions::write_gpt_structures;
use crate::iso::gpt::partition_entry::{EFI_SYSTEM_PARTITION_GUID, GptPartitionEntry};
use crate::iso::iso_image::IsoImage;
use crate::iso::iso_writer::{
copy_files, finalize_iso, write_boot_catalog_to_iso, write_descriptors, write_directories,
};
use crate::iso::mbr::create_mbr_for_gpt_hybrid;
pub struct IsoBuilder {
volume_id: Option<String>,
root: IsoDirectory,
boot_info: Option<BootInfo>,
current_lba: u32,
total_sectors: u32,
is_isohybrid: bool, uefi_catalog_path: Option<String>,
esp_lba: Option<u32>, esp_size_sectors: Option<u32>, }
impl Default for IsoBuilder {
fn default() -> Self {
Self::new()
}
}
impl IsoBuilder {
pub fn new() -> Self {
Self {
volume_id: None,
root: IsoDirectory::new(),
boot_info: None,
current_lba: 0,
total_sectors: 0,
is_isohybrid: false, uefi_catalog_path: None,
esp_lba: None,
esp_size_sectors: None,
}
}
pub fn set_volume_id(&mut self, volume_id: Option<String>) {
self.volume_id = volume_id;
}
pub fn add_file(&mut self, path_in_iso: &str, real_path: PathBuf) -> io::Result<()> {
let file_name = Path::new(path_in_iso)
.file_name()
.ok_or_else(|| io_error!(io::ErrorKind::InvalidInput, "Invalid file name"))?
.to_str()
.ok_or_else(|| io_error!(io::ErrorKind::InvalidInput, "Invalid file name"))?
.to_string();
let current_dir = ensure_directory_path(&mut self.root, path_in_iso)?;
let file_metadata = get_file_metadata(&real_path)?;
let file_size = file_metadata.len();
let file = IsoFile {
path: real_path,
size: file_size,
lba: 0,
};
current_dir
.children
.insert(file_name, IsoFsNode::File(file));
Ok(())
}
pub fn set_boot_info(&mut self, boot_info: BootInfo) {
self.boot_info = Some(boot_info);
}
pub fn set_isohybrid(&mut self, is_isohybrid: bool) {
self.is_isohybrid = is_isohybrid;
}
fn prepare_boot_entries(
&self,
esp_lba: Option<u32>,
esp_size_sectors: Option<u32>,
) -> io::Result<Vec<BootCatalogEntry>> {
let mut entries = Vec::new();
let bi = self.boot_info.as_ref();
if self.is_isohybrid {
if let (Some(lba), Some(size)) = (esp_lba, esp_size_sectors) {
entries.push(create_uefi_esp_boot_entry(lba, size)?);
}
} else if let Some(u) = bi.and_then(|b| b.uefi_boot.as_ref()) {
entries.push(create_uefi_boot_entry(&self.root, &u.destination_in_iso)?);
}
if let Some(b) = bi.and_then(|b| b.bios_boot.as_ref()) {
entries.push(create_bios_boot_entry(&self.root, &b.destination_in_iso)?);
}
Ok(entries)
}
fn write_hybrid_structures(
&self,
iso_file: &mut File,
total_lbas: u64,
esp_size_sectors: Option<u32>,
) -> io::Result<()> {
if total_lbas < 69 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"ISO image is too small for isohybrid with GPT ({} sectors, requires at least 69)",
total_lbas
),
));
}
iso_file.seek(SeekFrom::Start(0))?;
let mbr = create_mbr_for_gpt_hybrid(self.total_sectors, self.is_isohybrid)?;
mbr.write_to(iso_file)?;
if let Some(esp_size_sectors_val) = esp_size_sectors
&& esp_size_sectors_val > 0
{
let esp_partition_start_lba = ESP_START_LBA;
let esp_partition_end_lba = esp_partition_start_lba + esp_size_sectors_val - 1;
let esp_guid_str = EFI_SYSTEM_PARTITION_GUID;
let esp_unique_guid_str = Uuid::new_v4().to_string();
let partitions = vec![GptPartitionEntry::new(
esp_guid_str,
&esp_unique_guid_str,
esp_partition_start_lba as u64,
esp_partition_end_lba as u64,
"EFI System Partition",
0x0000000000000002, )];
write_gpt_structures(iso_file, total_lbas, &partitions)?;
iso_file.sync_data()?;
}
Ok(())
}
pub fn build(
&mut self,
iso_file: &mut File,
_iso_path: &Path,
esp_lba: Option<u32>,
esp_size_sectors: Option<u32>,
) -> io::Result<()> {
self.esp_lba = esp_lba;
self.esp_size_sectors = esp_size_sectors;
let reserved_sectors = if self.is_isohybrid {
ESP_START_LBA
} else {
16u32
};
let data_start_lba = reserved_sectors;
let boot_catalog_lba = 19;
self.current_lba = if self.is_isohybrid {
data_start_lba + esp_size_sectors.unwrap_or(0) } else {
boot_catalog_lba + 1 };
iso_file.seek(SeekFrom::Start(
(self.current_lba as u64) * ISO_SECTOR_SIZE as u64,
))?;
calculate_lbas(&mut self.current_lba, &mut self.root)?;
write_descriptors(
iso_file,
self.volume_id.as_deref(),
self.root.lba,
self.current_lba,
)?;
let boot_entries = self.prepare_boot_entries(esp_lba, esp_size_sectors)?;
write_boot_catalog_to_iso(iso_file, boot_catalog_lba, boot_entries)?;
write_directories(iso_file, &self.root, self.root.lba)?;
copy_files(iso_file, &self.root)?;
finalize_iso(iso_file, &mut self.total_sectors)?;
if self.is_isohybrid {
self.write_hybrid_structures(iso_file, self.total_sectors as u64, esp_size_sectors)?;
}
Ok(())
}
}
pub fn build_iso(
iso_path: &Path,
image: &IsoImage,
is_isohybrid: bool,
) -> io::Result<(PathBuf, Option<NamedTempFile>, File, Option<u32>)> {
let mut iso_builder = IsoBuilder::new();
iso_builder.set_volume_id(image.volume_id.clone());
iso_builder.set_isohybrid(is_isohybrid);
let mut temp_fat_file_holder: Option<NamedTempFile> = None;
let mut logical_fat_size_512_sectors: Option<u32> = None;
let mut iso_file = File::create(iso_path)?;
if let Some(uefi_boot) = &image.boot_info.uefi_boot {
iso_builder.uefi_catalog_path = Some(uefi_boot.destination_in_iso.clone());
if is_isohybrid {
let temp_fat_file = NamedTempFile::new()?;
let path = temp_fat_file.path().to_path_buf();
temp_fat_file_holder = Some(temp_fat_file);
let size_512_sectors =
fat::create_fat_image(&path, &uefi_boot.boot_image, &uefi_boot.kernel_image)?;
logical_fat_size_512_sectors = Some(size_512_sectors);
let calculated_esp_size_iso_sectors = size_512_sectors.div_ceil(4);
iso_builder.esp_lba = Some(ESP_START_LBA); iso_builder.esp_size_sectors = Some(calculated_esp_size_iso_sectors);
iso_file.seek(SeekFrom::Start(
ESP_START_LBA as u64 * crate::utils::ISO_SECTOR_SIZE as u64,
))?;
let mut temp_fat = std::fs::File::open(&path)?;
io::copy(&mut temp_fat, &mut iso_file)?;
}
}
for file in &image.files {
iso_builder.add_file(&file.destination, file.source.clone())?;
}
if let Some(bios_boot_info) = &image.boot_info.bios_boot {
iso_builder.add_file(
&bios_boot_info.destination_in_iso,
bios_boot_info.boot_image.clone(),
)?;
}
iso_builder.set_boot_info(image.boot_info.clone());
iso_builder.build(
&mut iso_file,
iso_path,
iso_builder.esp_lba,
iso_builder.esp_size_sectors,
)?;
let final_iso_file = iso_file;
Ok((
iso_path.to_path_buf(),
temp_fat_file_holder,
final_iso_file,
logical_fat_size_512_sectors, ))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_add_file() -> io::Result<()> {
let mut builder = IsoBuilder::new();
let temp_file = NamedTempFile::new()?;
let temp_path = temp_file.path().to_path_buf();
builder.add_file("root.txt", temp_path.clone())?;
assert!(builder.root.children.contains_key("root.txt"));
builder.add_file("dir1/nested.txt", temp_path.clone())?;
let dir1 = match builder.root.children.get("dir1") {
Some(IsoFsNode::Directory(dir)) => dir,
_ => panic!("dir1 was not created as a directory"),
};
assert!(dir1.children.contains_key("nested.txt"));
Ok(())
}
#[test]
fn test_calculate_lbas() -> io::Result<()> {
let mut root = IsoDirectory::new();
let mut current_lba = 20;
let mut subdir = IsoDirectory::new();
let file1 = IsoFile {
path: PathBuf::new(),
size: 1000,
lba: 0,
}; let file2 = IsoFile {
path: PathBuf::new(),
size: 3000,
lba: 0,
}; subdir
.children
.insert("file2.txt".to_string(), IsoFsNode::File(file2));
root.children
.insert("file1.txt".to_string(), IsoFsNode::File(file1));
root.children
.insert("subdir".to_string(), IsoFsNode::Directory(subdir));
calculate_lbas(&mut current_lba, &mut root)?;
assert_eq!(root.lba, 20);
match root.children.get("file1.txt") {
Some(IsoFsNode::File(f)) => assert_eq!(f.lba, 21),
_ => panic!("file1.txt not found"),
}
let (subdir_lba, file2_lba) = match root.children.get("subdir") {
Some(IsoFsNode::Directory(d)) => {
let file2_lba = match d.children.get("file2.txt") {
Some(IsoFsNode::File(f)) => f.lba,
_ => panic!("file2.txt not found"),
};
(d.lba, file2_lba)
}
_ => panic!("subdir not found"),
};
assert_eq!(subdir_lba, 22);
assert_eq!(file2_lba, 23);
assert_eq!(current_lba, 25);
Ok(())
}
#[test]
fn test_get_path_helpers() -> io::Result<()> {
let mut builder = IsoBuilder::new();
let mut temp_file = NamedTempFile::new()?;
temp_file.write_all(b"some data")?;
let temp_path = temp_file.path().to_path_buf();
builder.add_file("A/B/C.txt", temp_path)?;
builder.current_lba = 20;
calculate_lbas(&mut builder.current_lba, &mut builder.root)?;
let lba = crate::iso::builder_utils::get_lba_for_path(&builder.root, "A/B/C.txt")?;
let size = crate::iso::builder_utils::get_file_size_in_iso(&builder.root, "A/B/C.txt")?;
assert_eq!(lba, 23);
assert_eq!(size, 9);
assert!(crate::iso::builder_utils::get_lba_for_path(&builder.root, "A/D.txt").is_err());
Ok(())
}
}