use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use crate::iso::boot_catalog::{BootCatalogEntry, write_boot_catalog};
use crate::iso::dir_record::IsoDirEntry;
use crate::iso::fs_node::{IsoDirectory, IsoFsNode};
use crate::iso::volume_descriptor::{update_total_sectors_in_pvd, write_volume_descriptors};
use crate::utils::{ISO_SECTOR_SIZE, seek_to_lba};
pub fn write_descriptors(
iso_file: &mut File,
volume_id: Option<&str>,
root_lba: u32,
total_sectors: u32,
) -> io::Result<()> {
let root_entry = IsoDirEntry {
lba: root_lba,
size: ISO_SECTOR_SIZE as u32,
flags: 0x02,
name: ".",
};
write_volume_descriptors(iso_file, volume_id, total_sectors, &root_entry)
}
pub fn write_boot_catalog_to_iso(
iso_file: &mut File,
boot_catalog_lba: u32,
boot_entries: Vec<BootCatalogEntry>,
) -> io::Result<()> {
if !boot_entries.is_empty() {
iso_file.seek(SeekFrom::Start(
(boot_catalog_lba as u64) * ISO_SECTOR_SIZE as u64,
))?;
write_boot_catalog(iso_file, boot_entries)?;
}
Ok(())
}
pub fn write_directories(
iso_file: &mut File,
dir: &IsoDirectory,
parent_lba: u32,
) -> io::Result<()> {
seek_to_lba(iso_file, dir.lba)?;
let mut dir_entries = Vec::new();
dir_entries.push(IsoDirEntry {
lba: dir.lba,
size: ISO_SECTOR_SIZE as u32,
flags: 0x02,
name: ".",
});
dir_entries.push(IsoDirEntry {
lba: parent_lba,
size: ISO_SECTOR_SIZE as u32,
flags: 0x02,
name: "..",
});
for_sorted_children!(dir, |name, node| {
let (lba, size, flags) = match node {
IsoFsNode::File(file) => {
let file_size_u32 = u32::try_from(file.size).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"File '{}' is too large for ISO9660 (exceeds u32::MAX bytes)",
name
),
)
})?;
(file.lba, file_size_u32, 0x00)
}
IsoFsNode::Directory(subdir) => (subdir.lba, ISO_SECTOR_SIZE as u32, 0x02),
};
dir_entries.push(IsoDirEntry {
lba,
size,
flags,
name: name.as_str(),
});
});
let mut dir_sector = [0u8; ISO_SECTOR_SIZE];
let mut offset = 0;
for entry in &dir_entries {
let entry_bytes = entry.to_bytes();
dir_sector[offset..offset + entry_bytes.len()].copy_from_slice(&entry_bytes);
offset += entry_bytes.len();
}
iso_file.write_all(&dir_sector)?;
for_sorted_children!(dir, |_name, node| {
if let IsoFsNode::Directory(subdir) = node {
write_directories(iso_file, subdir, dir.lba)?;
}
});
Ok(())
}
pub fn copy_files(iso_file: &mut File, dir: &IsoDirectory) -> io::Result<()> {
for_sorted_children!(dir, |_name, node| {
match node {
IsoFsNode::File(file) => {
seek_to_lba(iso_file, file.lba)?;
let mut real_file = File::open(&file.path)?;
io::copy(&mut real_file, iso_file)?;
}
IsoFsNode::Directory(subdir) => {
copy_files(iso_file, subdir)?;
}
}
});
Ok(())
}
const PVD_LBA: u32 = 16;
pub fn write_boot_info_table(
iso_file: &mut File,
boot_image_lba: u32,
boot_image_size: u64,
) -> io::Result<()> {
let sector_base = boot_image_lba as u64 * ISO_SECTOR_SIZE as u64;
let checksum_start = sector_base + 64;
let mut checksum = 0u32;
if boot_image_size > 64 {
iso_file.seek(SeekFrom::Start(checksum_start))?;
let mut buf = [0u8; 4096];
let mut remaining = boot_image_size - 64;
while remaining > 0 {
let to_read = buf.len().min(remaining as usize);
iso_file.read_exact(&mut buf[..to_read])?;
for chunk in buf[..to_read].chunks_exact(4) {
checksum = checksum.wrapping_add(u32::from_le_bytes(chunk.try_into().unwrap()));
}
remaining -= to_read as u64;
}
}
let table_offset = sector_base + 8;
iso_file.seek(SeekFrom::Start(table_offset))?;
let mut table = [0u8; 56];
table[0..4].copy_from_slice(&PVD_LBA.to_le_bytes());
table[4..8].copy_from_slice(&boot_image_lba.to_le_bytes());
table[8..12].copy_from_slice(&(boot_image_size as u32).to_le_bytes());
table[12..16].copy_from_slice(&checksum.to_le_bytes());
iso_file.write_all(&table)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
use tempfile::NamedTempFile;
fn read_sector(file: &mut File, lba: u32) -> io::Result<[u8; ISO_SECTOR_SIZE]> {
let mut buf = [0u8; ISO_SECTOR_SIZE];
file.seek(SeekFrom::Start(lba as u64 * ISO_SECTOR_SIZE as u64))?;
file.read_exact(&mut buf)?;
Ok(buf)
}
#[test]
fn test_boot_info_table_structure() -> io::Result<()> {
let mut f = NamedTempFile::new()?;
let boot_lba = 50u32;
let boot_size: u64 = 2048;
let boot_offset = boot_lba as u64 * ISO_SECTOR_SIZE as u64;
let mut boot_data = vec![0u8; boot_size as usize];
for i in 64..boot_size as usize {
boot_data[i] = (i as u8).wrapping_mul(3).wrapping_add(0xAB);
}
f.seek(SeekFrom::Start(boot_offset))?;
f.write_all(&boot_data)?;
write_boot_info_table(f.as_file_mut(), boot_lba, boot_size)?;
let mut table = [0u8; 56];
f.seek(SeekFrom::Start(boot_offset + 8))?;
f.read_exact(&mut table)?;
assert_eq!(
u32::from_le_bytes(table[0..4].try_into().unwrap()),
16,
"PVD LBA should be 16"
);
assert_eq!(
u32::from_le_bytes(table[4..8].try_into().unwrap()),
boot_lba,
"boot image LBA mismatch"
);
assert_eq!(
u32::from_le_bytes(table[8..12].try_into().unwrap()),
boot_size as u32,
"boot image size mismatch"
);
assert_eq!(&table[16..56], &[0u8; 40], "reserved bytes not zero");
let mut expected_csum = 0u32;
for chunk in boot_data[64..].chunks_exact(4) {
expected_csum =
expected_csum.wrapping_add(u32::from_le_bytes(chunk.try_into().unwrap()));
}
let actual_csum = u32::from_le_bytes(table[12..16].try_into().unwrap());
assert_eq!(actual_csum, expected_csum, "checksum mismatch");
Ok(())
}
#[test]
fn test_boot_info_table_exact_64_bytes() -> io::Result<()> {
let mut f = NamedTempFile::new()?;
let boot_lba = 10u32;
let boot_size: u64 = 64;
let boot_offset = boot_lba as u64 * ISO_SECTOR_SIZE as u64;
f.seek(SeekFrom::Start(boot_offset))?;
f.write_all(&[0xFFu8; 2048])?;
write_boot_info_table(f.as_file_mut(), boot_lba, boot_size)?;
let mut table = [0u8; 56];
f.seek(SeekFrom::Start(boot_offset + 8))?;
f.read_exact(&mut table)?;
assert_eq!(
u32::from_le_bytes(table[12..16].try_into().unwrap()),
0,
"checksum should be 0 when file has no data beyond offset 64"
);
Ok(())
}
#[test]
fn test_boot_info_table_does_not_overflow_sector() -> io::Result<()> {
let mut f = NamedTempFile::new()?;
let boot_lba = 99u32;
let boot_offset = boot_lba as u64 * ISO_SECTOR_SIZE as u64;
let sector = [0xAAu8; ISO_SECTOR_SIZE as usize];
f.seek(SeekFrom::Start(boot_offset))?;
f.write_all(§or)?;
write_boot_info_table(f.as_file_mut(), boot_lba, 128)?;
let sector_read = read_sector(f.as_file_mut(), boot_lba)?;
assert_eq!(
§or_read[64..128],
&[0xAAu8; 64],
"boot-info-table wrote beyond offset 63"
);
Ok(())
}
}
pub fn finalize_iso(iso_file: &mut File, total_sectors: &mut u32) -> io::Result<()> {
let current_pos = iso_file.stream_position()?;
let remainder = current_pos % ISO_SECTOR_SIZE as u64;
if remainder != 0 {
let padding_bytes = ISO_SECTOR_SIZE as u64 - remainder;
io::copy(&mut io::repeat(0).take(padding_bytes), iso_file)?;
}
let final_pos = iso_file.stream_position()?;
let total_sectors_u64 = final_pos.div_ceil(ISO_SECTOR_SIZE as u64);
*total_sectors = u32::try_from(total_sectors_u64)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "ISO image too large"))?;
update_total_sectors_in_pvd(iso_file, *total_sectors)?;
Ok(())
}