crates/nornir-build-thing/src/iso_writer.rs
//! **Pure-Rust ISO-9660 + El-Torito writer** — masters a bootable `.iso` with
//! ZERO external process. This is the law-compliant replacement for the deleted
//! `xorriso`/`mkisofs`/`genisoimage` shell-out path: nothing here spawns a
//! subprocess, links a C library, or calls out to any ISO tool. The on-disk
//! ISO-9660 format is fully specified (ECM-119) and the El-Torito boot record is
//! a small, well-documented structure, so the whole image is emitted byte-for-byte
//! from Rust + `std`.
//!
//! # What it produces
//!
//! * A 2048-byte-sector ISO-9660 volume: a 16-sector system area, a **Primary
//! Volume Descriptor** (`CD001` at sector 16), an **El-Torito Boot Record**
//! volume descriptor, a Volume Descriptor Set Terminator, both path tables
//! (type-L little-endian + type-M big-endian), a recursive directory tree, and
//! the file data.
//! * An **El-Torito boot catalog**: a validation entry, a default/initial entry,
//! and — for a UEFI image — an EFI **section header + section entry**
//! (platform `0xEF`) pointing at the `efiboot.img` EFI System Partition. This is
//! what OVMF/UEFI firmware boots off a `-cdrom`, matching the `-e efiboot.img
//! -no-emul-boot` the old `xorriso` argv rendered.
//!
//! **Only when a boot image really resolved.** An image with nothing bootable
//! is mastered as a plain ISO-9660 data volume with no Boot Record VD and no
//! catalog — see [`BootImages`] for the catalog that used to be fabricated over
//! LBA 0 instead.
//!
//! # Layout
//!
//! ```text
//! BOOTABLE NOT BOOTABLE
//! sector 0..16 system area (zero) sector 0..16 system area (zero)
//! sector 16 Primary VD "CD001" sector 16 Primary VD "CD001"
//! sector 17 Boot Record VD sector 17 VD Set Terminator (255)
//! sector 18 VD Set Terminator sector 18 type-L path table (LE)
//! sector 19 type-L path table sector 19 type-M path table (BE)
//! sector 20 type-M path table sector 20.. directory extents
//! sector 21 El-Torito catalog sector .. file data
//! sector 22.. directory extents
//! sector .. file data
//! ```
//!
//! The volume-descriptor sequence has to be contiguous and terminated, so the
//! Boot Record VD cannot merely be left zeroed when there is nothing to boot —
//! everything after it moves up by one sector instead.
//!
//! Everything is content-deterministic (fixed timestamps), so the same input tree
//! masters byte-identically — the property a content-addressed build relies on.
use std::collections::BTreeMap;
use std::io::Read;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
/// ISO-9660 logical sector size (bytes).
pub const SECTOR: usize = 2048;
/// El-Torito platform id for a **BIOS** (x86) boot entry.
const PLATFORM_X86: u8 = 0x00;
/// El-Torito platform id for a **UEFI/EFI** boot entry.
const PLATFORM_EFI: u8 = 0xEF;
/// Round `n` up to the next multiple of [`SECTOR`].
fn round_sectors(n: u64) -> u64 {
n.div_ceil(SECTOR as u64)
}
/// A both-endian 16-bit field (ISO-9660 stores LE then BE).
fn both16(v: u16) -> [u8; 4] {
let le = v.to_le_bytes();
let be = v.to_be_bytes();
[le[0], le[1], be[0], be[1]]
}
/// A both-endian 32-bit field (ISO-9660 stores LE then BE).
fn both32(v: u32) -> [u8; 8] {
let le = v.to_le_bytes();
let be = v.to_be_bytes();
[le[0], le[1], le[2], le[3], be[0], be[1], be[2], be[3]]
}
/// The fixed 7-byte directory-record recording timestamp (deterministic:
/// 2024-01-01T00:00:00, GMT). Year is stored as years-since-1900.
const DIR_DATE: [u8; 7] = [124, 1, 1, 0, 0, 0, 0];
/// The fixed 17-byte volume-descriptor timestamp
/// (`YYYYMMDDHHMMSShh` + 1-byte GMT offset).
const VOL_DATE: &[u8; 17] = b"2024010100000000\0";
/// One node in the in-memory ISO directory tree.
struct DirNode {
/// Directory name component (empty for the root).
name: String,
/// Parent node index (root points at itself, index 0).
parent: usize,
/// Child directories: name → node index.
dirs: BTreeMap<String, usize>,
/// Files in this directory: name → where its bytes come from.
files: BTreeMap<String, FileSource>,
// ── assigned during layout ──
/// This directory's extent LBA.
lba: u32,
/// This directory's extent length in bytes.
len: u32,
/// 1-based path-table number.
path_no: u16,
}
/// A pure-Rust ISO-9660 + El-Torito image writer.
///
/// Build a tree with [`add_file`](Self::add_file) /
/// [`add_dir_recursive`](Self::add_dir_recursive), mark the El-Torito boot
/// image(s), then [`write`](Self::write) the `.iso`. No subprocess is ever
/// spawned.
pub struct IsoWriter {
volume_id: String,
nodes: Vec<DirNode>,
/// ISO-relative path (uppercased, `/`-joined) of the BIOS El-Torito boot image.
bios_boot: Option<String>,
/// ISO-relative path of the EFI El-Torito boot image (`efiboot.img`).
efi_boot: Option<String>,
/// Number of 512-byte sectors the BIOS boot entry loads.
bios_sectors: u16,
/// A real legacy-BIOS stage whose patch table the writer fills at build
/// time — see [`Self::set_bios_stage`]. When set, `bios_boot` names its
/// image and `bios_sectors` is derived from the image length.
bios_stage: Option<BiosStage>,
/// Whether (and when) the medium also carries a protective MBR + GPT whose
/// EFI System partition spans the staged `efiboot.img` — see [`GptHybrid`].
gpt_hybrid: GptHybrid,
/// The 440 bytes of boot code a HYBRID medium carries at LBA 0 — see
/// [`Self::set_hybrid_mbr`]. `None` leaves LBA 0 as ISO-9660 leaves it:
/// all zeros, no `55 AA`, unbootable off any hard-disk device.
hybrid_mbr: Option<Vec<u8>>,
/// The shape the LAST [`build_image`](Self::build_image) produced — the
/// applied layout a caller puts on its receipt.
layout: Option<MediumLayout>,
}
/// **When the medium is a GPT hybrid** — an `.iso` that is ALSO a valid
/// GPT-partitioned disk image, the way `xorriso -isohybrid-gpt-basdat` and
/// systemd's `mkosi` shape theirs.
///
/// A plain ISO-9660/El-Torito medium describes its EFI System Partition only in
/// the boot catalog, whose sector count is a **u16 of 512-byte sectors**
/// ([`MAX_BOOT_IMAGE_BYTES`] = 33 553 920 B). A hybrid additionally lays a
/// protective MBR at byte 0, a primary GPT header + partition array in the
/// ISO's unused 32 KiB system area, and a backup array + header in a tail
/// appended after the last file extent. Its one partition is an EFI System
/// partition (`C12A7328-F81F-11D2-BA4B-00A0C93EC93B`) whose byte range is
/// EXACTLY the staged ESP image, so firmware reading the medium as a 512-byte-
/// sector disk (a USB stick, a raw `-drive`) finds the whole FAT through the
/// GPT — with no 32 MiB ceiling at all.
///
/// In a hybrid the El-Torito EFI entry is still emitted (firmware booting the
/// medium as optical media reads 2048-byte blocks, where the GPT at byte 512 is
/// invisible, and goes through the catalog), but an ESP past the u16 ceiling
/// has its catalog sector count **clamped to 65 535**, and the writer RECORDS
/// that in [`MediumLayout::efi`] rather than hiding it. What a clamped count
/// buys from optical media is firmware-defined: EDK2/OVMF's `ElTorito.c` sizes a
/// no-emulation child device as `SectorCount × 2048` bytes (it multiplies by
/// the MEDIA block size, not El-Torito's 512), so it exposes up to
/// [`EDK2_OPTICAL_ESP_REACH`] = 128 MiB. MEASURED 2026-08-22 on oden (QEMU /
/// OVMF_CODE_4M, `korp-installer/tests/hybrid_medium_boot.rs`): a 46 889 984 B
/// ESP whose `BOOTX64.EFI` sits behind a 40 MiB filler booted from `-cdrom`
/// through the clamped entry AND from a raw virtio disk through the GPT, the
/// chainloaded stage echoing the run's nonce both times. Firmware that honours
/// the nominal count stops at 32 MiB on optical media — and boots the same
/// medium off USB/disk through the GPT.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GptHybrid {
/// A plain ISO-9660 + El-Torito medium. An ESP over the El-Torito ceiling
/// is a hard error naming the numbers (the 2026-08-03 silent-clamp lesson).
#[default]
Off,
/// Plain while the ESP fits El-Torito; a GPT hybrid (with the clamped,
/// recorded catalog entry) once it does not. A medium under the ceiling is
/// **byte-identical** to [`GptHybrid::Off`] — gunnar's and holger's media
/// do not change shape because korp's outgrew the catalog.
WhenEspExceedsElTorito,
/// Always lay the GPT, whatever the ESP's size — a medium meant for USB
/// sticks as much as for optical drives.
Always,
}
/// How far EDK2-derived firmware (OVMF, and the AMI/Insyde firmwares built on
/// it) reads a no-emulation El-Torito image whose catalog count is clamped to
/// 65 535: `ElTorito.c` computes the child device as `SectorCount × media block
/// size`, and optical media have 2048-byte blocks, so 65 535 × 2048 =
/// **134 215 680 B**. Beyond this even OVMF's optical path truncates the ESP;
/// the GPT path (USB/disk) has no such reach limit.
pub const EDK2_OPTICAL_ESP_REACH: u64 = u16::MAX as u64 * SECTOR as u64;
/// **The ISO-9660 SYSTEM AREA** — the first 16 sectors (32 KiB) of every
/// ISO-9660 volume, which the standard reserves and this writer leaves zero.
/// It is where a hybrid medium's boot sector, its GPT and the disk-flavoured
/// copy of the BIOS stage live, which is why making a medium hybrid costs it
/// **no bytes at all**.
pub const SYSTEM_AREA_BYTES: usize = 16 * SECTOR;
/// **Where the disk-flavoured copy of the BIOS stage sits**, in 512-byte LBAs
/// — inside [`SYSTEM_AREA_BYTES`], past LBA 0 (the boot sector) and past the
/// GPT's primary header + array (LBAs 1..=33), so a hybrid MBR and a
/// [`GptHybrid`] medium can be laid over the same bytes.
///
/// It is a CONSTANT and not a placement because the boot sector's 440 bytes of
/// code are assembled by the caller (`korp_boot_bios::emit_mbr`, which this
/// crate cannot depend on — the loader depends on THIS crate for
/// [`crate::bios_patch`], not the other way round). One number both sides
/// spell from here, never twice.
pub const HYBRID_STAGE_LBA: u64 = 34;
/// Where the hybrid medium's second boot path landed — what a receipt says
/// when an `.iso` is ALSO a bootable disk.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HybridMbrPlacement {
/// 512-byte LBA of the disk-flavoured stage copy ([`HYBRID_STAGE_LBA`]).
pub stage_lba: u64,
/// Its length in 512-byte sectors — what the MBR code reads.
pub stage_sectors: u16,
/// The kernel's LBA as patched into THAT copy's table (the El-Torito
/// copy's LBA × 4: the same bytes, counted in 512-byte sectors).
pub kernel_lba: u32,
/// The initramfs' LBA as patched into that copy (0 = none).
pub initrd_lba: u32,
}
/// Where the El-Torito catalog put the EFI boot image, and how it described it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EfiBootPlacement {
/// The image's load RBA (2048-byte ISO sector).
pub lba: u32,
/// The image's byte length on the medium.
pub len: u64,
/// The sector count the catalog STATES (512-byte virtual sectors).
pub catalog_sectors: u16,
/// `true` when `len` needed more than `u16::MAX` sectors and the count was
/// clamped — only ever possible in a [`GptHybrid`] medium; the plain
/// writer refuses instead.
pub clamped: bool,
}
/// The GPT an [`IsoWriter`] laid over a hybrid medium — its one EFI System
/// partition, in 512-byte LBAs of the medium read as a disk.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GptEspPartition {
/// First LBA of the ESP partition (inclusive).
pub first_lba: u64,
/// Last LBA of the ESP partition (inclusive).
pub last_lba: u64,
/// The medium's last LBA (where the backup GPT header sits).
pub disk_last_lba: u64,
/// The disk GUID (on-disk mixed-endian order), derived from content so the
/// mastering stays deterministic.
pub disk_guid: [u8; 16],
/// The ESP's unique partition GUID (on-disk order).
pub partition_guid: [u8; 16],
}
impl GptEspPartition {
/// Byte offset of the ESP partition on the medium.
pub fn offset_bytes(&self) -> u64 {
self.first_lba * GPT_LBA
}
/// Byte length of the ESP partition.
pub fn len_bytes(&self) -> u64 {
(self.last_lba - self.first_lba + 1) * GPT_LBA
}
}
/// **The shape a mastered medium has** — what [`IsoWriter::layout`] hands back
/// after a build, so the composer's receipt states what was laid rather than
/// what was intended.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MediumLayout {
/// Total image bytes (a whole number of 2048-byte sectors, tail included).
pub total_bytes: u64,
/// The El-Torito EFI entry, when an EFI boot image was marked.
pub efi: Option<EfiBootPlacement>,
/// The GPT's ESP partition, when the medium is a hybrid.
pub gpt: Option<GptEspPartition>,
/// `true` when the medium is UEFI-only and the writer laid the BIOS
/// refusal stub ([`bios_refusal_sector`]) as the El-Torito default entry
/// (and, on a hybrid, as the protective MBR's boot code).
pub bios_refusal: bool,
/// The real legacy-BIOS stage, when one was declared through
/// [`IsoWriter::set_bios_stage`]: where it landed, what the catalog says,
/// and what its patch table was filled with.
pub bios: Option<BiosStagePlacement>,
/// The HYBRID boot sector, when one was declared through
/// [`IsoWriter::set_hybrid_mbr`] — the medium is then bootable as a DISK
/// as well as as a cdrom.
pub hybrid_mbr: Option<HybridMbrPlacement>,
}
/// **A legacy-BIOS first stage the writer patches** — the loader image, and
/// the payloads whose placement it must be told about. See
/// [`crate::bios_patch`] for the table format and
/// [`IsoWriter::set_bios_stage`] for what the writer does with it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BiosStage {
/// ISO path of the staged stage image (e.g. `bios/stage.bin`); it becomes
/// the El-Torito default (80x86, no-emulation) entry.
pub image: String,
/// ISO path of the bzImage the stage loads (e.g. `vmlinuz`).
pub kernel: String,
/// ISO path of the initramfs the stage loads, if any.
pub initrd: Option<String>,
/// The kernel command line the stage hands to the kernel.
pub cmdline: String,
}
/// Where a declared [`BiosStage`] landed and what was patched into it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BiosStagePlacement {
/// The stage image's load RBA (2048-byte ISO sector).
pub lba: u32,
/// The stage image's byte length on the medium.
pub len: u32,
/// The sector count the catalog states (512-byte virtual sectors) — the
/// whole image, rounded up.
pub catalog_sectors: u16,
/// The kernel's LBA as patched into the table.
pub kernel_lba: u32,
/// The kernel's byte length as patched.
pub kernel_len: u32,
/// The initramfs' LBA as patched (0 = none).
pub initrd_lba: u32,
/// The initramfs' byte length as patched (0 = none).
pub initrd_len: u32,
/// The command line's byte length as patched.
pub cmdline_len: u32,
}
impl MediumLayout {
/// One receipt line saying what this medium is — plain or hybrid, and for a
/// hybrid exactly what each boot path gets.
pub fn describe(&self) -> String {
let base = self.describe_boot_paths();
let hybrid = match self.hybrid_mbr {
None => String::new(),
Some(h) => format!(
"; HYBRID: 55 AA boot sector at LBA 0 chainloading a disk-flavoured copy of the stage \
at 512-byte LBA {} ({} sectors, sector_shift 9, kernel LBA {}, initrd LBA {}) — this \
.iso boots as a DISK too, so a cloud that imports a disk image can run the installer",
h.stage_lba, h.stage_sectors, h.kernel_lba, h.initrd_lba
),
};
if let Some(b) = self.bios {
format!(
"{base}; legacy BIOS: real stage at LBA {} ({} B, catalog {} sectors) patched with \
kernel LBA {} ({} B), initrd LBA {} ({} B), cmdline {} B{hybrid}",
b.lba, b.len, b.catalog_sectors, b.kernel_lba, b.kernel_len, b.initrd_lba, b.initrd_len, b.cmdline_len
)
} else if self.bios_refusal {
format!(
"{base}; legacy BIOS/CSM: UEFI-only REFUSAL stub ({BIOS_REFUSAL_FILE}) as the El-Torito \
default entry — SeaBIOS prints a sentence and halts instead of running FAT bytes"
)
} else {
base
}
}
fn describe_boot_paths(&self) -> String {
match (self.efi, self.gpt) {
(Some(efi), Some(gpt)) => {
let optical = if efi.clamped {
format!(
"El-Torito entry CLAMPED to {} sectors (spec-nominal {} B; EDK2/OVMF \
optical reach {} B{})",
efi.catalog_sectors,
MAX_BOOT_IMAGE_BYTES,
EDK2_OPTICAL_ESP_REACH,
if efi.len > EDK2_OPTICAL_ESP_REACH {
" — EXCEEDED: optical boot of this medium is unsupported, boot it as a disk"
} else {
""
}
)
} else {
format!("El-Torito entry {} sectors (exact)", efi.catalog_sectors)
};
format!(
"GPT HYBRID medium, {} B: ESP {} B at ISO LBA {} = GPT ESP partition LBA {}..={} \
(disk/USB boot reads the whole FAT through the GPT); {optical}",
self.total_bytes, efi.len, efi.lba, gpt.first_lba, gpt.last_lba,
)
}
(Some(efi), None) => format!(
"plain ISO-9660/El-Torito medium, {} B: ESP {} B at ISO LBA {}, catalog {} sectors",
self.total_bytes, efi.len, efi.lba, efi.catalog_sectors
),
(None, _) => format!("ISO-9660 medium, {} B, no EFI boot image", self.total_bytes),
}
}
}
/// Where a staged file's bytes come from: a host path (everything a caller
/// stages) or bytes the writer itself synthesised (the BIOS refusal stub —
/// see [`bios_refusal_sector`]). One enum rather than a temp file, so the
/// writer never touches the host filesystem to master something it made up.
#[derive(Clone)]
enum FileSource {
Host(PathBuf),
Bytes(Vec<u8>),
}
impl FileSource {
fn len(&self) -> Result<u64> {
match self {
FileSource::Host(p) => Ok(std::fs::metadata(p)
.with_context(|| format!("iso writer: stat {}", p.display()))?
.len()),
FileSource::Bytes(b) => Ok(b.len() as u64),
}
}
fn read(&self, cap: usize) -> Result<Vec<u8>> {
match self {
FileSource::Host(p) => {
let mut f = std::fs::File::open(p)
.with_context(|| format!("iso writer: open {}", p.display()))?;
let mut buf = Vec::with_capacity(cap);
f.read_to_end(&mut buf)?;
Ok(buf)
}
FileSource::Bytes(b) => Ok(b.clone()),
}
}
}
/// A file's assigned data location, filled during layout.
struct FilePlacement {
lba: u32,
len: u32,
src: FileSource,
}
/// The ISO-root name of the BIOS refusal stub a UEFI-only medium carries (8.3,
/// what [`iso_file_name`] would make of it, minus the `;1`).
pub const BIOS_REFUSAL_FILE: &str = "BIOSREF.BIN";
/// The text the BIOS refusal stub prints — on the VGA text console (INT 10h
/// teletype) AND byte-for-byte on COM1 (port `0x3F8`), so a headless box with
/// a serial console reads the same sentence a KVM graphics console shows.
pub const BIOS_REFUSAL_TEXT: &str =
"This medium boots UEFI only. Legacy BIOS (SeaBIOS/CSM) cannot start it: switch the machine to UEFI firmware.";
/// **The BIOS El-Torito image a UEFI-only medium carries: an honest refusal.**
///
/// # Why a medium with no BIOS payload needs a BIOS entry at all
///
/// El-Torito's *default/initial* entry is platform `0x00` (80x86) and every
/// BIOS reads it. Until 2026-08-28 a UEFI-only medium pointed that entry at the
/// FAT `efiboot.img` (OVMF ignores the default and takes the `0xEF` section),
/// so SeaBIOS loaded the first 512-byte sectors of a FAT filesystem and
/// **executed them as code**. MEASURED twice: gunnar 2026-08-12 ("garbled
/// COLOUR CHARS on the console" — the confetti was FAT bytes running as
/// instructions) and gunnar-installer-…-nowg.iso on UpCloud 2026-08-27
/// (UpCloud boots SeaBIOS only: *"noll utdata, installeraren startar aldrig"*).
/// Both times the operator had NOTHING to read — no sentence, no refusal, no
/// hint that the medium was fine and the firmware was the mismatch.
///
/// This is not BIOS support (a BIOS boot path for a bzImage + initramfs is a
/// real loader and a product decision); it is the one honest sentence the
/// firmware can be made to print. The catalog's default entry now loads THIS
/// sector at `0000:7C00`; it prints [`BIOS_REFUSAL_TEXT`] and halts. The EFI
/// section is untouched — OVMF/UEFI boots exactly as before.
///
/// # The code, hand-assembled (real mode, 16-bit, loaded at `0x7C00`)
///
/// ```text
/// 7C00 FA cli
/// 7C01 31 C0 xor ax, ax
/// 7C03 8E D8 mov ds, ax
/// 7C05 8E D0 mov ss, ax
/// 7C07 BC 00 7C mov sp, 0x7C00
/// 7C0A FB sti
/// 7C0B BE 24 7C mov si, 0x7C24 ; the message below
/// 7C0E AC L: lodsb
/// 7C0F 84 C0 test al, al
/// 7C11 74 0D jz H
/// 7C13 BA F8 03 mov dx, 0x3F8 ; COM1
/// 7C16 EE out dx, al
/// 7C17 B4 0E mov ah, 0x0E ; INT 10h teletype
/// 7C19 BB 07 00 mov bx, 0x0007 ; page 0, light grey
/// 7C1C CD 10 int 0x10
/// 7C1E EB EE jmp L
/// 7C20 FA H: cli
/// 7C21 F4 hlt
/// 7C22 EB FC jmp H
/// 7C24 <text> 0D 0A 00
/// ```
///
/// Pure bytes, no assembler in the build: the offsets are asserted by a test
/// below (`the_bios_refusal_sector_is_a_valid_boot_sector`), and the sector
/// ends in the `55 AA` signature so the same bytes serve as the boot code of
/// the hybrid medium's protective MBR ([`lay_hybrid_gpt`]) — a UEFI-only
/// medium dd'd to a USB stick and booted by a BIOS says the same sentence.
pub fn bios_refusal_sector() -> [u8; 512] {
let mut s = [0u8; 512];
let code: [u8; 36] = [
0xFA, // cli
0x31, 0xC0, // xor ax,ax
0x8E, 0xD8, // mov ds,ax
0x8E, 0xD0, // mov ss,ax
0xBC, 0x00, 0x7C, // mov sp,0x7C00
0xFB, // sti
0xBE, 0x24, 0x7C, // mov si,0x7C24
0xAC, // L: lodsb
0x84, 0xC0, // test al,al
0x74, 0x0D, // jz H
0xBA, 0xF8, 0x03, // mov dx,0x3F8
0xEE, // out dx,al
0xB4, 0x0E, // mov ah,0x0E
0xBB, 0x07, 0x00, // mov bx,0x0007
0xCD, 0x10, // int 0x10
0xEB, 0xEE, // jmp L
0xFA, // H: cli
0xF4, // hlt
0xEB, 0xFC, // jmp H
];
s[..code.len()].copy_from_slice(&code);
let mut off = code.len(); // == 0x24
for b in BIOS_REFUSAL_TEXT.bytes().chain(*b"\r\n") {
s[off] = b;
off += 1;
}
s[off] = 0; // NUL terminator the loop stops on
s[510] = 0x55;
s[511] = 0xAA;
s
}
impl IsoWriter {
/// A fresh writer for a volume with the given id (uppercased + trimmed to the
/// 32-char ISO d-character volume-id field on write).
pub fn new(volume_id: impl Into<String>) -> Self {
IsoWriter {
volume_id: volume_id.into(),
nodes: vec![DirNode {
name: String::new(),
parent: 0,
dirs: BTreeMap::new(),
files: BTreeMap::new(),
lba: 0,
len: 0,
path_no: 1,
}],
bios_boot: None,
efi_boot: None,
bios_sectors: 4,
bios_stage: None,
gpt_hybrid: GptHybrid::Off,
hybrid_mbr: None,
layout: None,
}
}
/// Choose whether the medium is laid as a [`GptHybrid`] (default:
/// [`GptHybrid::Off`] — the plain medium, and a hard error past the
/// El-Torito ceiling).
pub fn set_gpt_hybrid(&mut self, mode: GptHybrid) {
self.gpt_hybrid = mode;
}
/// **Make the medium HYBRID** — an `.iso` that is also a bootable DISK.
///
/// `mbr_code` is the boot sector's first [`crate::bios_patch::MBR_CODE_LEN`]
/// bytes, assembled by the caller from
/// `korp_boot_bios::emit_mbr(`[`HYBRID_STAGE_LBA`]`, stage_sectors)`: it
/// relocates itself, reads the stage from that LBA with INT 13h AH=42h and
/// jumps to it. The writer lays the rest — a partition table, the `55 AA`
/// signature, and a SECOND copy of the declared [`BiosStage`] at
/// [`HYBRID_STAGE_LBA`] whose patch table counts in 512-byte sectors
/// ([`crate::bios_patch::SECTOR_SHIFT_DISK`]) instead of 2048-byte ones.
///
/// # Why a second copy and not one stage that guesses
///
/// The stage's patch table holds LBAs, and an LBA is only a number until
/// you say how big a sector is. Off a cdrom the firmware's INT 13h counts
/// 2048-byte sectors; off a disk it counts 512-byte ones. The same file
/// therefore needs two tables, and a stage that asked the firmware
/// (INT 13h AH=48h) would be a THIRD opinion about a fact the writer
/// already knows with certainty. Two patched copies of one image, from one
/// emitter, is the same trick [`set_bios_stage`](Self::set_bios_stage)
/// already plays — and both copies live in the ISO's reserved 32 KiB
/// system area ([`SYSTEM_AREA_BYTES`]), so the medium does not grow by a
/// byte.
///
/// Refused at build time when no real BIOS stage is declared: a boot
/// sector that chainloads nothing is worse than no boot sector, because
/// firmware stops looking once it finds `55 AA`.
pub fn set_hybrid_mbr(&mut self, mbr_code: Vec<u8>) {
self.hybrid_mbr = Some(mbr_code);
}
/// The shape the last [`build_image`](Self::build_image) / [`write`](Self::write)
/// produced; `None` before any build.
pub fn layout(&self) -> Option<&MediumLayout> {
self.layout.as_ref()
}
/// Place `host` on the ISO at the `/`-separated ISO path `iso_path`
/// (e.g. `EFI/BOOT/BOOTX64.EFI`). Intermediate directories are created.
pub fn add_file(&mut self, iso_path: &str, host: impl Into<PathBuf>) -> Result<()> {
let host = host.into();
let comps: Vec<&str> = iso_path
.split('/')
.filter(|c| !c.is_empty() && *c != ".")
.collect();
if comps.is_empty() {
bail!("iso writer: empty iso path for {}", host.display());
}
let (dirs, file) = comps.split_at(comps.len() - 1);
let mut cur = 0usize;
for d in dirs {
let key = iso_dir_name(d);
if let Some(&idx) = self.nodes[cur].dirs.get(&key) {
cur = idx;
} else {
let idx = self.nodes.len();
self.nodes.push(DirNode {
name: key.clone(),
parent: cur,
dirs: BTreeMap::new(),
files: BTreeMap::new(),
lba: 0,
len: 0,
path_no: 0,
});
self.nodes[cur].dirs.insert(key, idx);
cur = idx;
}
}
let fname = iso_file_name(file[0]);
self.nodes[cur].files.insert(fname, FileSource::Host(host));
Ok(())
}
/// Recursively stage a host directory `host` under the ISO path prefix
/// `iso_prefix` (empty for the volume root). Symlinks are followed as files.
pub fn add_dir_recursive(&mut self, iso_prefix: &str, host: &Path) -> Result<()> {
for entry in std::fs::read_dir(host)
.with_context(|| format!("iso writer: read staging dir {}", host.display()))?
{
let entry = entry?;
let name = entry.file_name();
let name = name.to_string_lossy();
let child_iso = if iso_prefix.is_empty() {
name.to_string()
} else {
format!("{iso_prefix}/{name}")
};
let ft = entry.file_type()?;
if ft.is_dir() {
self.add_dir_recursive(&child_iso, &entry.path())?;
} else {
self.add_file(&child_iso, entry.path())?;
}
}
Ok(())
}
/// Mark an already-added file as the BIOS El-Torito boot image (no-emulation).
pub fn set_bios_boot(&mut self, iso_path: &str, load_sectors: u16) {
self.bios_boot = Some(normalize_iso_path(iso_path));
self.bios_sectors = load_sectors.max(1);
}
/// **Declare a REAL legacy-BIOS stage** (already added to the tree) as the
/// El-Torito default (80x86, no-emulation) entry, and have the writer fill
/// its [`crate::bios_patch`] table at build time with where `stage.kernel`
/// and `stage.initrd` landed on the volume plus `stage.cmdline`.
///
/// This is the isolinux boot-info-table trick generalised: the writer is
/// the ONE party that knows every file's LBA, so the loader parses no
/// ISO-9660 at all. The catalog's sector count is the whole stage image,
/// rounded up to 512-byte sectors. A medium with a declared stage never
/// gets the refusal stub ([`bios_refusal_sector`]) — it has a real BIOS
/// path — and the EFI section is untouched, so UEFI firmware boots exactly
/// as before: ONE medium, two El-Torito entries.
pub fn set_bios_stage(&mut self, stage: BiosStage) {
self.bios_boot = Some(normalize_iso_path(&stage.image));
self.bios_stage = Some(stage);
}
/// Mark an already-added file as the EFI (`0xEF`) El-Torito boot image — the
/// `efiboot.img` EFI System Partition OVMF boots.
pub fn set_efi_boot(&mut self, iso_path: &str) {
self.efi_boot = Some(normalize_iso_path(iso_path));
}
/// Master the ISO to `out`, returning the total image size in bytes. Pure
/// Rust: builds the whole byte image in memory and writes it once.
pub fn write(&mut self, out: &Path) -> Result<u64> {
let image = self.build_image()?;
std::fs::write(out, &image)
.with_context(|| format!("iso writer: write {}", out.display()))?;
Ok(image.len() as u64)
}
/// Build the complete ISO byte image (the testable core of [`write`]).
pub fn build_image(&mut self) -> Result<Vec<u8>> {
// ── layout ──────────────────────────────────────────────────────────
// A BOOTABLE image: PVD 16, Boot Record VD 17, terminator 18, path
// tables 19+20, boot catalog 21, then directory extents and file data.
//
// A NON-bootable image drops the Boot Record VD and the catalog, and
// everything after moves up: PVD 16, terminator 17, path tables 18+19,
// then the extents. The volume-descriptor sequence has to be contiguous
// and terminated, so the boot record cannot simply be left as a zeroed
// sector — a reader would take type 0 with no `CD001` as a malformed
// descriptor. See `BootImages` for why an ISO with nothing bootable no
// longer gets these structures at all.
let bootable = self.bios_boot.is_some() || self.efi_boot.is_some();
// A UEFI-only medium gets an honest BIOS default entry — the refusal
// stub — instead of the FAT ESP's first sectors running as x86 code
// under SeaBIOS. See `bios_refusal_sector` for the two measurements.
// Idempotent: a second `build_image` re-inserts the same key.
let bios_refusal = self.bios_boot.is_none() && self.efi_boot.is_some();
if bios_refusal {
self.nodes[0].files.insert(
iso_file_name(BIOS_REFUSAL_FILE),
FileSource::Bytes(bios_refusal_sector().to_vec()),
);
}
const PVD_LBA: u32 = 16;
let boot_record_lba: u32 = 17;
let terminator_lba: u32 = if bootable { 18 } else { 17 };
let path_l_lba: u32 = terminator_lba + 1;
let path_m_lba: u32 = path_l_lba + 1;
let boot_catalog_lba: u32 = path_m_lba + 1;
let mut next_lba: u32 = if bootable { boot_catalog_lba + 1 } else { path_m_lba + 1 };
// BFS order over directories → path-table numbering + extent placement.
let order = self.bfs_order();
// Compute each directory's extent length.
for &ni in &order {
let len = self.dir_extent_len(ni);
self.nodes[ni].len = len as u32;
}
// Assign path-table numbers (1-based, root == 1) and directory LBAs.
for (pos, &ni) in order.iter().enumerate() {
self.nodes[ni].path_no = (pos + 1) as u16;
}
for &ni in &order {
self.nodes[ni].lba = next_lba;
next_lba += round_sectors(self.nodes[ni].len as u64) as u32;
}
// Place every file's data. Deterministic order: directory BFS, files sorted.
let mut placements: BTreeMap<String, FilePlacement> = BTreeMap::new();
for &ni in &order {
let files: Vec<(String, FileSource)> = self.nodes[ni]
.files
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let iso_dir = self.node_iso_path(ni);
for (fname, src) in files {
let len = src.len()?;
let lba = next_lba;
next_lba += round_sectors(len.max(1)) as u32;
let key = if iso_dir.is_empty() {
strip_version(&fname)
} else {
format!("{iso_dir}/{}", strip_version(&fname))
};
placements.insert(
key,
FilePlacement { lba, len: len as u32, src },
);
}
}
// ── the real BIOS stage: fill its patch table NOW, after placement ──
// The writer is the one party that knows where the kernel and the
// initramfs landed; the loader reads the numbers off its own image and
// parses no ISO-9660. The patched bytes replace the host source so the
// serialization below writes what the stage will run with.
// The disk-flavoured twin of the stage, when the medium is hybrid: the
// SAME image bytes with a table that counts 512-byte sectors. Produced
// in the one place that knows where the payloads landed.
let mut hybrid_stage: Option<(Vec<u8>, HybridMbrPlacement)> = None;
let bios_placement: Option<BiosStagePlacement> = match self.bios_stage.clone() {
None => None,
Some(stage) => {
let kernel = lookup_placement(&placements, &normalize_iso_path(&stage.kernel))?;
let (kernel_lba, kernel_len) = (kernel.lba, kernel.len);
let (initrd_lba, initrd_len) = match stage.initrd.as_deref() {
Some(p) => {
let i = lookup_placement(&placements, &normalize_iso_path(p))?;
(i.lba, i.len)
}
None => (0, 0),
};
let key = normalize_iso_path(&stage.image);
let pl = placements.get_mut(&key).with_context(|| {
format!("iso writer: BIOS stage '{key}' was not staged on the ISO")
})?;
let mut bytes = pl.src.read(pl.len as usize)?;
let off = crate::bios_patch::find_table(&bytes)
.with_context(|| format!("iso writer: BIOS stage '{key}'"))?;
let table = crate::bios_patch::BiosPatchTable {
kernel_lba,
kernel_len,
initrd_lba,
initrd_len,
cmdline: stage.cmdline.clone(),
sector_shift: crate::bios_patch::SECTOR_SHIFT_CD,
};
table
.write_into(&mut bytes, off)
.with_context(|| format!("iso writer: patching BIOS stage '{key}'"))?;
let catalog_sectors = u16::try_from((bytes.len() as u64).div_ceil(512)).map_err(|_| {
anyhow::anyhow!("iso writer: BIOS stage '{key}' is {} B, over the El-Torito ceiling", bytes.len())
})?;
// A stage must fit below the conventional-memory hole the BIOS
// loads it into (0x7C00 upward, under 0xA0000) with room for its
// own bounce buffers; 64 KiB is far beyond any real stage.
if bytes.len() > 64 * 1024 {
bail!("iso writer: BIOS stage '{key}' is {} B; a real-mode stage must stay under 64 KiB", bytes.len());
}
let placement = BiosStagePlacement {
lba: pl.lba,
len: bytes.len() as u32,
catalog_sectors,
kernel_lba,
kernel_len,
initrd_lba,
initrd_len,
cmdline_len: stage.cmdline.len() as u32,
};
// ── the hybrid twin ──────────────────────────────────────
// Off a disk, INT 13h counts 512-byte sectors, so every LBA in
// the table is the El-Torito one × 4 and `sector_shift` says
// 9. Nothing else about the image changes.
if self.hybrid_mbr.is_some() {
const PER_ISO_SECTOR: u32 = (SECTOR / GPT_LBA as usize) as u32;
let mut twin = pl.src.read(pl.len as usize)?;
let disk = crate::bios_patch::BiosPatchTable {
kernel_lba: kernel_lba * PER_ISO_SECTOR,
kernel_len,
initrd_lba: if initrd_lba == 0 { 0 } else { initrd_lba * PER_ISO_SECTOR },
initrd_len,
cmdline: stage.cmdline.clone(),
sector_shift: crate::bios_patch::SECTOR_SHIFT_DISK,
};
let toff = crate::bios_patch::find_table(&twin)
.with_context(|| format!("iso writer: hybrid twin of BIOS stage '{key}'"))?;
disk.write_into(&mut twin, toff)
.with_context(|| format!("iso writer: patching the hybrid twin of '{key}'"))?;
// div_ceil, never truncating: an MBR that reads floor(len/512)
// sectors leaves the stage's tail — its STRINGS, and on a
// shorter image its table — unloaded, and the loader then
// prints garbage or nothing. `emit_stage` already pads to a
// whole sector; this is what makes that a property rather
// than a coincidence.
let stage_sectors = u16::try_from(twin.len().div_ceil(GPT_LBA as usize).max(1))
.map_err(|_| anyhow::anyhow!("iso writer: the hybrid stage copy is too long to chainload"))?;
let end = HYBRID_STAGE_LBA as usize * GPT_LBA as usize
+ stage_sectors as usize * GPT_LBA as usize;
if end > SYSTEM_AREA_BYTES {
bail!(
"iso writer: the hybrid stage copy ({} B at LBA {HYBRID_STAGE_LBA}) ends at byte {end}, past the ISO system area's {SYSTEM_AREA_BYTES} B — it would overwrite the volume descriptors",
twin.len()
);
}
let placement = HybridMbrPlacement {
stage_lba: HYBRID_STAGE_LBA,
stage_sectors,
kernel_lba: disk.kernel_lba,
initrd_lba: disk.initrd_lba,
};
hybrid_stage = Some((twin, placement));
}
pl.src = FileSource::Bytes(bytes);
self.bios_sectors = catalog_sectors.max(1);
Some(placement)
}
};
// ── resolve El-Torito boot-image placements ──────────────────────────
// `(placement, 512-byte sector count)`: a caller's BIOS image loads
// `bios_sectors`; the refusal stub is ONE sector by construction.
let bios: Option<(&FilePlacement, u16)> = match self.bios_boot.as_ref() {
Some(p) => Some((lookup_placement(&placements, p)?, self.bios_sectors)),
None if bios_refusal => Some((lookup_placement(&placements, BIOS_REFUSAL_FILE)?, 1)),
None => None,
};
let efi = self
.efi_boot
.as_ref()
.map(|p| lookup_placement(&placements, p))
.transpose()?;
// ── the El-Torito EFI entry: exact, or (hybrid only) clamped + recorded ──
// The ONLY conversions from a byte length to a catalog sector count are
// `el_torito_sectors` (refuses past the ceiling) and
// `el_torito_sectors_clamped` (clamps and SAYS so) — and the second is
// reachable only when a GPT is also laid, so no medium ever carries a
// clamped count as its sole description of the ESP.
let hybrid = match (self.gpt_hybrid, efi.as_ref()) {
(GptHybrid::Off, _) | (_, None) => false,
(GptHybrid::Always, Some(_)) => true,
(GptHybrid::WhenEspExceedsElTorito, Some(p)) => p.len as u64 > MAX_BOOT_IMAGE_BYTES,
};
let efi_entry: Option<EfiBootPlacement> = match efi.as_ref() {
None => None,
Some(p) if hybrid => {
let (catalog_sectors, clamped) = el_torito_sectors_clamped(p.len as u64);
Some(EfiBootPlacement { lba: p.lba, len: p.len as u64, catalog_sectors, clamped })
}
Some(p) => {
let catalog_sectors =
el_torito_sectors(p.len as u64, "EFI (efiboot.img)").with_context(|| {
format!(
"iso writer: building the El-Torito boot catalog for {}",
self.efi_boot.as_deref().unwrap_or("(no EFI boot image)")
)
})?;
Some(EfiBootPlacement { lba: p.lba, len: p.len as u64, catalog_sectors, clamped: false })
}
};
// A hybrid carries its backup GPT (array + header) in a tail after the
// last extent; the PVD's volume space covers the tail so the ISO-9660
// volume and the disk image end on the same byte.
let total_sectors = if hybrid { next_lba + GPT_TAIL_ISO_SECTORS } else { next_lba };
// ── serialize ────────────────────────────────────────────────────────
let mut img = vec![0u8; total_sectors as usize * SECTOR];
// Primary Volume Descriptor.
let root = &self.nodes[0];
let pvd = self.primary_volume_descriptor(
total_sectors,
root.lba,
root.len,
path_l_lba,
path_m_lba,
self.path_table_len(&order),
);
write_sector(&mut img, PVD_LBA, &pvd);
// El-Torito Boot Record volume descriptor + boot catalog — emitted ONLY
// when a boot image really resolved. An image with nothing bootable is a
// plain data volume, not a volume that claims a boot catalog it has no
// image for.
//
// Fallible: an oversized boot image stops the build here, with its size
// and the ceiling in the message, instead of being silently truncated
// into a catalog that firmware reads as "no bootable device".
if let Some(images) = BootImages::new(
bios.map(|(p, sectors)| (p.lba, sectors)),
efi_entry.map(|e| (e.lba, e.catalog_sectors)),
) {
let catalog = boot_catalog(&images)?;
write_sector(&mut img, boot_record_lba, &boot_record_vd(boot_catalog_lba));
write_sector(&mut img, boot_catalog_lba, &catalog);
}
// Volume Descriptor Set Terminator.
let mut term = vec![0u8; SECTOR];
term[0] = 255;
term[1..6].copy_from_slice(b"CD001");
term[6] = 1;
write_sector(&mut img, terminator_lba, &term);
// Path tables.
let (l_table, m_table) = self.path_tables(&order);
write_at(&mut img, path_l_lba, &l_table);
write_at(&mut img, path_m_lba, &m_table);
// Directory extents.
for &ni in &order {
let extent = self.dir_extent(ni, &placements);
write_at(&mut img, self.nodes[ni].lba, &extent);
}
// File data.
for pl in placements.values() {
let buf = pl.src.read(pl.len as usize)?;
let off = pl.lba as usize * SECTOR;
img[off..off + buf.len()].copy_from_slice(&buf);
}
// ── the GPT hybrid: protective MBR + primary GPT in the system area,
// backup GPT in the tail, ONE EFI System partition == the ESP bytes ──
let gpt = match efi_entry {
Some(e) if hybrid => Some(lay_hybrid_gpt(&mut img, &self.volume_id, e, bios_refusal)?),
_ => None,
};
// ── the HYBRID boot sector: the same .iso, bootable as a DISK ────────
// Laid LAST, because the GPT above owns the same 512 bytes and this is
// the boot code both shapes carry: a protective GPT MBR and a plain
// hybrid MBR differ in their PARTITION TABLE, never in whether the
// firmware is given something to run.
let hybrid_mbr = match (self.hybrid_mbr.clone(), hybrid_stage) {
(None, _) => None,
(Some(_), None) => bail!(
"iso writer: a hybrid MBR was declared but this medium has no real legacy-BIOS stage (`set_bios_stage`) — a boot sector that chainloads nothing is WORSE than none, because firmware stops looking the moment it finds 55 AA"
),
(Some(code), Some((twin, placement))) => {
if code.len() > crate::bios_patch::MBR_CODE_LEN {
bail!(
"iso writer: {} bytes of MBR boot code do not fit in the {} the partition table leaves",
code.len(),
crate::bios_patch::MBR_CODE_LEN
);
}
write_lba(&mut img, placement.stage_lba, &twin);
let total_lbas = (img.len() as u64 / GPT_LBA).min(u32::MAX as u64) as u32;
let mbr = &mut img[0..GPT_LBA as usize];
mbr[..code.len()].copy_from_slice(&code);
if gpt.is_none() {
// ONE partition spanning the whole medium, starting at LBA 0
// — what `isohybrid` writes, and what every firmware and
// partition reader has accepted from an ISO for fifteen
// years. With a GPT the 0xEE protective entry
// `lay_hybrid_gpt` already laid stays: two partition tables
// claiming the same bytes is what a GPT hybrid must not be.
let p = 446;
mbr[p] = 0x80; // bootable
mbr[p + 1] = 0x00; // CHS first: head 0, sector 1, cylinder 0
mbr[p + 2] = 0x01;
mbr[p + 3] = 0x00;
mbr[p + 4] = 0x83; // Linux
mbr[p + 5] = 0xFF; // CHS last: max (the LBA fields are the truth)
mbr[p + 6] = 0xFF;
mbr[p + 7] = 0xFF;
mbr[p + 8..p + 12].copy_from_slice(&0u32.to_le_bytes());
mbr[p + 12..p + 16].copy_from_slice(&total_lbas.to_le_bytes());
}
let mbr = &mut img[0..GPT_LBA as usize];
mbr[510] = 0x55;
mbr[511] = 0xAA;
Some(placement)
}
};
self.layout = Some(MediumLayout {
total_bytes: img.len() as u64,
efi: efi_entry,
gpt,
bios_refusal,
bios: bios_placement,
hybrid_mbr,
});
Ok(img)
}
/// BFS order of directory node indices (root first).
fn bfs_order(&self) -> Vec<usize> {
let mut order = Vec::with_capacity(self.nodes.len());
let mut queue = std::collections::VecDeque::new();
queue.push_back(0usize);
while let Some(ni) = queue.pop_front() {
order.push(ni);
for &child in self.nodes[ni].dirs.values() {
queue.push_back(child);
}
}
order
}
/// The `/`-joined ISO path of a directory node ("" for the root).
fn node_iso_path(&self, ni: usize) -> String {
if ni == 0 {
return String::new();
}
let mut parts = Vec::new();
let mut cur = ni;
while cur != 0 {
parts.push(self.nodes[cur].name.clone());
cur = self.nodes[cur].parent;
}
parts.reverse();
parts.join("/")
}
/// Length in bytes of a directory's extent (`.` + `..` + children, unpadded).
fn dir_extent_len(&self, ni: usize) -> usize {
let mut len = dir_record_len(1) * 2; // "." and ".."
let node = &self.nodes[ni];
for name in node.dirs.keys() {
len += dir_record_len(name.len());
}
for name in node.files.keys() {
len += dir_record_len(name.len());
}
len
}
/// Serialize a directory's extent (padded to a whole sector), reading each
/// file record's (lba, len) from the assigned placement map.
fn dir_extent(&self, ni: usize, places: &BTreeMap<String, FilePlacement>) -> Vec<u8> {
let node = &self.nodes[ni];
let parent = &self.nodes[node.parent];
let iso_dir = self.node_iso_path(ni);
let mut buf = Vec::new();
// "." → this directory.
buf.extend_from_slice(&dir_record(&[0x00], node.lba, node.len, true));
// ".." → parent directory.
buf.extend_from_slice(&dir_record(&[0x01], parent.lba, parent.len, true));
// Child directories (sorted by BTreeMap).
for (name, &idx) in &node.dirs {
let c = &self.nodes[idx];
buf.extend_from_slice(&dir_record(name.as_bytes(), c.lba, c.len, true));
}
// Files (sorted) — look up the LBA/len assigned in build_image.
for name in node.files.keys() {
let key = if iso_dir.is_empty() {
strip_version(name)
} else {
format!("{iso_dir}/{}", strip_version(name))
};
let (lba, len) = places
.get(&key)
.map(|p| (p.lba, p.len))
.unwrap_or((0, 0));
buf.extend_from_slice(&dir_record(name.as_bytes(), lba, len, false));
}
// Pad to a whole sector.
pad_to_sector(&mut buf);
buf
}
/// Total (unpadded) length of the type-L / type-M path table.
fn path_table_len(&self, order: &[usize]) -> u32 {
order
.iter()
.map(|&ni| path_record_len(self.nodes[ni].name.len().max(1)) as u32)
.sum()
}
/// Build the type-L (LE) and type-M (BE) path tables, each padded to a sector.
fn path_tables(&self, order: &[usize]) -> (Vec<u8>, Vec<u8>) {
let mut l = Vec::new();
let mut m = Vec::new();
for &ni in order {
let node = &self.nodes[ni];
let parent_no = self.nodes[node.parent].path_no;
let (name_bytes, name_len): (&[u8], usize) = if ni == 0 {
(&[0x00], 1)
} else {
(node.name.as_bytes(), node.name.len())
};
l.extend_from_slice(&path_record(name_bytes, name_len, node.lba, parent_no, false));
m.extend_from_slice(&path_record(name_bytes, name_len, node.lba, parent_no, true));
}
pad_to_sector(&mut l);
pad_to_sector(&mut m);
(l, m)
}
/// The Primary Volume Descriptor sector.
#[allow(clippy::too_many_arguments)]
fn primary_volume_descriptor(
&self,
total_sectors: u32,
root_lba: u32,
root_len: u32,
path_l_lba: u32,
path_m_lba: u32,
path_table_len: u32,
) -> Vec<u8> {
let mut s = vec![0u8; SECTOR];
s[0] = 1; // PVD type
s[1..6].copy_from_slice(b"CD001");
s[6] = 1; // version
// system id (8..40) + volume id (40..72): A/d-chars, space-padded.
fill_dchars(&mut s[8..40], "");
fill_dchars(&mut s[40..72], &self.volume_id);
s[80..88].copy_from_slice(&both32(total_sectors)); // volume space size
s[120..124].copy_from_slice(&both16(1)); // volume set size
s[124..128].copy_from_slice(&both16(1)); // volume sequence number
s[128..132].copy_from_slice(&both16(SECTOR as u16)); // logical block size
s[132..140].copy_from_slice(&both32(path_table_len)); // path table size
s[140..144].copy_from_slice(&path_l_lba.to_le_bytes()); // type-L path table
s[148..152].copy_from_slice(&path_m_lba.to_be_bytes()); // type-M path table
// root directory record (34 bytes) at 156..190.
let root_rec = dir_record(&[0x00], root_lba, root_len, true);
s[156..156 + root_rec.len()].copy_from_slice(&root_rec);
// text id fields: spaces.
for r in [190..318, 318..446, 446..574, 574..702] {
for b in &mut s[r] {
*b = b' ';
}
}
for r in [702..739, 739..776, 776..813] {
for b in &mut s[r] {
*b = b' ';
}
}
s[813..830].copy_from_slice(VOL_DATE); // creation
s[830..847].copy_from_slice(VOL_DATE); // modification
s[847..864].copy_from_slice(&[b'0'; 17]); // expiration = unset
s[847 + 16] = 0;
s[864..881].copy_from_slice(&[b'0'; 17]); // effective = unset
s[864 + 16] = 0;
s[881] = 1; // file structure version
s
}
}
// ── free helpers ────────────────────────────────────────────────────────────
/// Bytes of a directory record for a name of `name_len` bytes (even-padded).
fn dir_record_len(name_len: usize) -> usize {
let base = 33 + name_len;
base + (base & 1) // pad to even
}
/// Serialize one directory record.
fn dir_record(name: &[u8], extent_lba: u32, data_len: u32, is_dir: bool) -> Vec<u8> {
let base = 33 + name.len();
let total = base + (base & 1);
let mut r = vec![0u8; total];
r[0] = total as u8;
r[1] = 0; // ext attr length
r[2..10].copy_from_slice(&both32(extent_lba));
r[10..18].copy_from_slice(&both32(data_len));
r[18..25].copy_from_slice(&DIR_DATE);
r[25] = if is_dir { 0x02 } else { 0x00 };
r[26] = 0; // file unit size
r[27] = 0; // interleave gap
r[28..32].copy_from_slice(&both16(1)); // volume sequence number
r[32] = name.len() as u8;
r[33..33 + name.len()].copy_from_slice(name);
r
}
/// Bytes of a path-table record for a name of `name_len` bytes (even-padded).
fn path_record_len(name_len: usize) -> usize {
let base = 8 + name_len;
base + (base & 1)
}
/// Serialize one path-table record (`big_endian` selects type-M vs type-L).
fn path_record(name: &[u8], name_len: usize, extent_lba: u32, parent_no: u16, big_endian: bool) -> Vec<u8> {
let base = 8 + name_len;
let total = base + (base & 1);
let mut r = vec![0u8; total];
r[0] = name_len as u8;
r[1] = 0; // ext attr length
if big_endian {
r[2..6].copy_from_slice(&extent_lba.to_be_bytes());
r[6..8].copy_from_slice(&parent_no.to_be_bytes());
} else {
r[2..6].copy_from_slice(&extent_lba.to_le_bytes());
r[6..8].copy_from_slice(&parent_no.to_le_bytes());
}
r[8..8 + name_len].copy_from_slice(&name[..name_len]);
r
}
/// The El-Torito Boot Record volume descriptor (sector 17).
fn boot_record_vd(catalog_lba: u32) -> Vec<u8> {
let mut s = vec![0u8; SECTOR];
s[0] = 0; // boot record type
s[1..6].copy_from_slice(b"CD001");
s[6] = 1; // version
let id = b"EL TORITO SPECIFICATION";
s[7..7 + id.len()].copy_from_slice(id); // boot system id (32 bytes, zero-padded)
s[71..75].copy_from_slice(&catalog_lba.to_le_bytes()); // boot catalog pointer (LE)
s
}
/// One El-Torito *virtual* sector. The catalog's sector counts are in these,
/// **not** in 2048-byte ISO sectors.
pub const EL_TORITO_VIRTUAL_SECTOR: u64 = 512;
/// The largest boot image El-Torito can describe: the sector-count field is 16
/// bits of 512-byte virtual sectors, so `65535 × 512` = **33 553 920 bytes**.
///
/// This is a hard limit of the on-disc format, not a policy of ours. There is
/// nothing to raise it to.
pub const MAX_BOOT_IMAGE_BYTES: u64 = u16::MAX as u64 * EL_TORITO_VIRTUAL_SECTOR;
/// A boot image's length as an El-Torito 512-byte sector count, or an error
/// naming every number needed to act on it.
///
/// # Why this is an error and not a clamp
///
/// It used to be `.min(u16::MAX)`. An oversized boot image was therefore
/// **silently truncated**: the catalog described the first 33 553 920 bytes of
/// an ESP that was larger, the ISO mastered without a word, and the firmware —
/// handed a block device that stops in the middle of the filesystem — reported
/// `no bootable device`. No diagnostic, no log line, nothing in the build
/// output to search for. For an appliance that is the worst failure there is:
/// it is discovered on real hardware, by whoever is standing in front of it,
/// and it looks identical to a dead USB stick.
///
/// It was also not hypothetical. Measured 2026-08-03: gunnar's appliance ESP is
/// **33 471 488 B**, which is 82 KiB — 0.25 % — under the ceiling. One kernel
/// bump crosses it.
///
/// So the size is checked at **build** time, where the numbers are known and a
/// human is watching, and the message carries the actual size, the ceiling and
/// the overflow so nobody has to go and work out which of the three they need.
fn el_torito_sectors(len: u64, what: &str) -> Result<u16> {
if len > MAX_BOOT_IMAGE_BYTES {
bail!(
"the {what} boot image is {len} bytes, which is {over} bytes ({over_kib} KiB) over \
El-Torito's hard ceiling of {MAX_BOOT_IMAGE_BYTES} bytes.\n\
The boot catalog's sector count is 16 bits of {EL_TORITO_VIRTUAL_SECTOR}-byte \
virtual sectors ({max_sectors} × {EL_TORITO_VIRTUAL_SECTOR}), so a larger image \
cannot be described at all — it is a limit of the ISO-9660/El-Torito format and \
there is no field to widen.\n\
This used to be truncated silently and the ISO mastered anyway; the firmware then \
said `no bootable device` and nothing else. Shrink the boot image instead: \
compress the initramfs, or drop what the appliance does not boot with.",
over = len - MAX_BOOT_IMAGE_BYTES,
over_kib = (len - MAX_BOOT_IMAGE_BYTES).div_ceil(1024),
max_sectors = u16::MAX,
);
}
// `max(1)`: a zero-length image would describe zero sectors, and firmware
// reads that as "no image" rather than as an error.
Ok(len.div_ceil(EL_TORITO_VIRTUAL_SECTOR).max(1) as u16)
}
/// The hybrid's conversion: the exact count while it fits, `u16::MAX` and
/// `clamped = true` when it does not. Reachable ONLY from the GPT-hybrid arm of
/// [`IsoWriter::build_image`], where the GPT carries the full ESP and the clamp
/// is recorded on the [`MediumLayout`] — never as a medium's sole description.
fn el_torito_sectors_clamped(len: u64) -> (u16, bool) {
let sectors = len.div_ceil(EL_TORITO_VIRTUAL_SECTOR).max(1);
if sectors > u16::MAX as u64 {
(u16::MAX, true)
} else {
(sectors as u16, false)
}
}
// ── the GPT hybrid ──────────────────────────────────────────────────────────
//
// The on-disk GPT (UEFI 2.x §5.3) laid over the `.iso`, in 512-byte LBAs:
//
// LBA 0 protective MBR (one 0xEE partition spanning the medium)
// LBA 1 primary GPT header
// LBA 2..=33 primary partition array (128 × 128 B) ← all inside the
// ISO's 32 KiB
// system area
// … the ISO-9660 volume (descriptors, tables, extents) — the
// ESP's extent is the ONE partition, at ISO LBA × 4
// last-32..=last-1 backup partition array ← the appended tail
// last backup GPT header
//
// korp-installer's `gpt.rs` lays the same structures on a TARGET DISK (three
// partitions, a disk the installer writes); this lays them over a medium the
// writer is mastering. They cannot share code: korp-installer's PID-1 builds
// must not link this crate (see its Cargo.toml), and this crate is what
// korp-installer's forge consumes — so the ~40 lines of header/entry/MBR
// serialization below are the sibling of gpt.rs's, and
// `the_hybrid_gpt_parses_back_and_spans_exactly_the_esp` proves these bytes
// against an independent reader rather than trusting the twin.
/// A GPT logical block: the medium read as a 512-byte-sector disk.
pub const GPT_LBA: u64 = 512;
/// Partition entries in the array (the UEFI-standard 128).
const GPT_N_ENTRIES: u32 = 128;
/// One partition entry (the UEFI-standard 128 bytes).
const GPT_ENTRY_SIZE: u32 = 128;
/// LBAs the 128 × 128 B array spans.
const GPT_ARRAY_LBAS: u64 = (GPT_N_ENTRIES as u64 * GPT_ENTRY_SIZE as u64) / GPT_LBA;
/// First usable LBA: past the MBR (0), the primary header (1) and the array (2..=33).
const GPT_FIRST_USABLE_LBA: u64 = 2 + GPT_ARRAY_LBAS;
/// The tail that carries the backup array (32 LBAs) + backup header (1 LBA),
/// rounded up to whole ISO sectors: 33 × 512 = 16 896 B → 9 × 2048 = 18 432 B.
const GPT_TAIL_ISO_SECTORS: u32 = ((GPT_ARRAY_LBAS + 1) * GPT_LBA).div_ceil(SECTOR as u64) as u32;
/// The EFI System Partition type GUID (`C12A7328-F81F-11D2-BA4B-00A0C93EC93B`),
/// in on-disk mixed-endian byte order.
pub const GPT_ESP_TYPE_GUID: [u8; 16] = [
0x28, 0x73, 0x2A, 0xC1, 0x1F, 0xF8, 0xD2, 0x11, 0xBA, 0x4B, 0x00, 0xA0, 0xC9, 0x3E, 0xC9, 0x3B,
];
/// Lay the protective MBR + primary + backup GPT over an already-serialized
/// image (which must already carry the [`GPT_TAIL_ISO_SECTORS`] tail), with one
/// EFI System partition spanning exactly the EFI boot image's bytes.
fn lay_hybrid_gpt(
img: &mut [u8],
volume_id: &str,
efi: EfiBootPlacement,
bios_refusal: bool,
) -> Result<GptEspPartition> {
let total = img.len() as u64;
if total % GPT_LBA != 0 {
bail!("iso writer: image of {total} bytes is not a whole number of {GPT_LBA}-byte LBAs");
}
let disk_lbas = total / GPT_LBA;
let disk_last_lba = disk_lbas - 1;
let backup_array_lba = disk_last_lba - GPT_ARRAY_LBAS;
let last_usable_lba = backup_array_lba - 1;
// The ESP: ISO sector × 4 is its first LBA; its length is the FILE's length
// rounded up to whole LBAs (a FAT image is always a whole number of 512-byte
// sectors, so this is exact for every ESP the composer produces).
let first_lba = efi.lba as u64 * (SECTOR as u64 / GPT_LBA);
let last_lba = first_lba + efi.len.div_ceil(GPT_LBA).max(1) - 1;
if first_lba < GPT_FIRST_USABLE_LBA || last_lba > last_usable_lba {
bail!(
"iso writer: the ESP at LBA {first_lba}..={last_lba} does not sit inside the GPT's usable \
range {GPT_FIRST_USABLE_LBA}..={last_usable_lba} — the layout is wrong, not the payload"
);
}
// Deterministic GUIDs from the content the medium is made of — the same tree
// masters byte-identically, hybrid or not.
let (disk_guid, partition_guid) = hybrid_guids(volume_id, first_lba, last_lba);
// Partition entry array (primary + backup share the bytes).
let mut entries = vec![0u8; (GPT_N_ENTRIES * GPT_ENTRY_SIZE) as usize];
{
let slot = &mut entries[0..GPT_ENTRY_SIZE as usize];
slot[0..16].copy_from_slice(&GPT_ESP_TYPE_GUID);
slot[16..32].copy_from_slice(&partition_guid);
slot[32..40].copy_from_slice(&first_lba.to_le_bytes());
slot[40..48].copy_from_slice(&last_lba.to_le_bytes());
// attributes (48..56) = 0; name (56..128) UTF-16LE.
let mut off = 56;
for u in "EFI System".encode_utf16() {
slot[off..off + 2].copy_from_slice(&u.to_le_bytes());
off += 2;
}
}
let array_crc = crc32(&entries);
// Protective MBR at LBA 0.
let mbr = &mut img[0..GPT_LBA as usize];
// Boot code (bytes 0..440): the same UEFI-only refusal the El-Torito default
// entry carries, so a hybrid medium dd'd to a stick and booted by a BIOS says
// the sentence instead of "no bootable device" or silence. The partition
// table below and the 55 AA signature are what make it a protective MBR.
if bios_refusal {
mbr[..440].copy_from_slice(&bios_refusal_sector()[..440]);
}
let p = 446;
mbr[p] = 0x00; // not bootable
mbr[p + 1] = 0x00; // CHS first: head 0, sector 2, cylinder 0
mbr[p + 2] = 0x02;
mbr[p + 3] = 0x00;
mbr[p + 4] = 0xEE; // GPT protective
mbr[p + 5] = 0xFF; // CHS last: max
mbr[p + 6] = 0xFF;
mbr[p + 7] = 0xFF;
mbr[p + 8..p + 12].copy_from_slice(&1u32.to_le_bytes());
mbr[p + 12..p + 16].copy_from_slice(&((disk_lbas - 1).min(u32::MAX as u64) as u32).to_le_bytes());
mbr[510] = 0x55;
mbr[511] = 0xAA;
// Primary header (LBA 1) + array (LBA 2..).
let primary = gpt_header(1, disk_last_lba, last_usable_lba, &disk_guid, 2, array_crc);
write_lba(img, 1, &primary);
write_lba(img, 2, &entries);
// Backup array + header in the tail.
let backup = gpt_header(disk_last_lba, 1, last_usable_lba, &disk_guid, backup_array_lba, array_crc);
write_lba(img, backup_array_lba, &entries);
write_lba(img, disk_last_lba, &backup);
Ok(GptEspPartition { first_lba, last_lba, disk_last_lba, disk_guid, partition_guid })
}
/// A 512-byte GPT header with both CRC32s computed; `my_lba`/`alt_lba` swap
/// between the primary and the backup.
fn gpt_header(
my_lba: u64,
alt_lba: u64,
last_usable: u64,
disk_guid: &[u8; 16],
entries_lba: u64,
array_crc: u32,
) -> Vec<u8> {
let mut h = vec![0u8; GPT_LBA as usize];
h[0..8].copy_from_slice(b"EFI PART");
h[8..12].copy_from_slice(&0x0001_0000u32.to_le_bytes()); // revision 1.0
h[12..16].copy_from_slice(&92u32.to_le_bytes()); // header size
h[24..32].copy_from_slice(&my_lba.to_le_bytes());
h[32..40].copy_from_slice(&alt_lba.to_le_bytes());
h[40..48].copy_from_slice(&GPT_FIRST_USABLE_LBA.to_le_bytes());
h[48..56].copy_from_slice(&last_usable.to_le_bytes());
h[56..72].copy_from_slice(disk_guid);
h[72..80].copy_from_slice(&entries_lba.to_le_bytes());
h[80..84].copy_from_slice(&GPT_N_ENTRIES.to_le_bytes());
h[84..88].copy_from_slice(&GPT_ENTRY_SIZE.to_le_bytes());
h[88..92].copy_from_slice(&array_crc.to_le_bytes());
let hdr_crc = crc32(&h[0..92]);
h[16..20].copy_from_slice(&hdr_crc.to_le_bytes());
h
}
/// Disk + partition GUIDs derived from the medium's identity and the ESP's
/// span (SHA-256, then v4 version + RFC-4122 variant bits, in on-disk
/// mixed-endian order) — unique per distinct medium, identical for a byte-
/// identical re-master.
fn hybrid_guids(volume_id: &str, first_lba: u64, last_lba: u64) -> ([u8; 16], [u8; 16]) {
use sha2::Digest;
let mut h = sha2::Sha256::new();
h.update(b"nornir-build-thing iso hybrid gpt\0");
h.update(volume_id.as_bytes());
h.update(first_lba.to_le_bytes());
h.update(last_lba.to_le_bytes());
let d = h.finalize();
let stamp = |bytes: &[u8]| -> [u8; 16] {
let mut g = [0u8; 16];
g.copy_from_slice(bytes);
g[7] = (g[7] & 0x0F) | 0x40; // version 4
g[8] = (g[8] & 0x3F) | 0x80; // RFC-4122 variant
g
};
(stamp(&d[0..16]), stamp(&d[16..32]))
}
/// CRC-32/ISO-HDLC (the zlib CRC the GPT spec mandates).
fn crc32(bytes: &[u8]) -> u32 {
let mut h = crc32fast::Hasher::new();
h.update(bytes);
h.finalize()
}
/// Write `data` at 512-byte LBA `lba` of the image.
fn write_lba(img: &mut [u8], lba: u64, data: &[u8]) {
let off = (lba * GPT_LBA) as usize;
img[off..off + data.len()].copy_from_slice(data);
}
/// The boot image(s) an El-Torito catalog describes: a BIOS entry `(lba,
/// 512-byte sector count)`, an EFI entry `(lba, byte length)`, or both.
///
/// # Why this is a type and not two `Option` arguments
///
/// [`boot_catalog`] used to take the two options directly and carry a third arm:
///
/// ```text
/// (None, None) => (0, 4), // no boot image at all
/// ```
///
/// followed unconditionally by `c[32] = 0x88` (bootable) and `c[33] = 0x00`
/// (no emulation). An ISO carrying nothing bootable therefore got a fully
/// well-formed, checksummed El-Torito catalog whose default entry told firmware
/// to load four 512-byte sectors from **LBA 0** — the ISO's own zeroed system
/// area. Verified on a real probe image. Nothing said a word at build time and
/// the structures were the ones firmware trusts most.
///
/// `(None, None)` is now unrepresentable: [`BootImages::new`] returns `None`,
/// and [`IsoWriter::build_image`] then emits a plain ISO-9660 data volume with
/// no Boot Record volume descriptor and no catalog — which is also exactly what
/// `mkisofs` produces from an argv carrying neither `-b` nor `-e`, i.e. what
/// [`IsoBuild::master_args`](crate::iso::IsoBuild::master_args) renders for such
/// a descriptor.
struct BootImages {
/// BIOS (platform `0x00`) boot image: `(load RBA, 512-byte sector count)`.
bios: Option<(u32, u16)>,
/// EFI (platform `0xEF`) boot image: `(load RBA, 512-byte sector count)` —
/// the count already converted by [`el_torito_sectors`] (exact, or refused)
/// or [`el_torito_sectors_clamped`] (hybrid only, recorded), so the catalog
/// serializer cannot do a conversion of its own.
efi: Option<(u32, u16)>,
}
impl BootImages {
/// `Some` only when at least one boot image is present — the constructor that
/// makes "a bootable catalog for nothing" impossible to build.
fn new(bios: Option<(u32, u16)>, efi: Option<(u32, u16)>) -> Option<Self> {
if bios.is_none() && efi.is_none() {
return None;
}
Some(BootImages { bios, efi })
}
}
/// The El-Torito boot catalog: validation entry, default/initial entry, and —
/// when an EFI image is present — an EFI section header + entry.
///
/// Reached only when there is something to boot; see [`BootImages`] for the arm
/// that used to fabricate a bootable entry over LBA 0.
///
/// Takes sector COUNTS, never byte lengths: the only conversions from a byte
/// length to a sector count live in [`el_torito_sectors`] (refuses past the
/// ceiling — see it for what the silence used to cost) and
/// [`el_torito_sectors_clamped`] (the GPT-hybrid arm, which records the clamp),
/// both called by [`IsoWriter::build_image`] before the catalog is serialized.
fn boot_catalog(images: &BootImages) -> Result<Vec<u8>> {
let (bios, efi) = (images.bios, images.efi);
let mut c = vec![0u8; SECTOR];
// ── Validation entry (bytes 0..32) ──
c[0] = 0x01; // header id
c[1] = PLATFORM_X86; // platform id for the initial/default entry
// id string (bytes 4..28) left zero.
c[30] = 0x55;
c[31] = 0xAA;
// 16-bit checksum so the whole 32-byte entry sums to 0 (mod 2^16).
let sum = validation_checksum(&c[0..32]);
c[28..30].copy_from_slice(&sum.to_le_bytes());
// ── Default / initial entry (bytes 32..64) ──
// A UEFI-only image still needs a valid default entry; point it at whichever
// boot image exists (OVMF ignores the BIOS default and uses the EFI section).
// El-Torito sector counts are in 512-byte VIRTUAL sectors (not 2048-byte ISO
// sectors), so the firmware's block device spans the whole boot image.
let (def_lba, def_sectors) = match (bios, efi) {
(Some(b), _) => b,
(None, Some((lba, sectors))) => (lba, sectors),
// Unreachable by construction: `BootImages` cannot be built empty, and
// `efi_sectors` is `Some` exactly when `efi` is. If this ever fires the
// constructor's invariant broke, and a catalog full of zeros pointed at
// LBA 0 is precisely the failure this whole type exists to prevent — so
// it stops the build instead of shipping.
(None, None) => bail!(
"iso writer: an El-Torito boot catalog was requested with no boot image. A catalog \
with no image marks LBA 0 bootable and firmware then loads the ISO's own system \
area; refusing to write one."
),
};
c[32] = 0x88; // bootable
c[33] = 0x00; // no emulation
c[34..36].copy_from_slice(&0u16.to_le_bytes()); // load segment (default)
c[36] = 0x00; // system type
c[38..40].copy_from_slice(&def_sectors.to_le_bytes()); // sector count
c[40..44].copy_from_slice(&def_lba.to_le_bytes()); // load RBA (2048-sector)
// ── EFI section (bytes 64..96 + 96..128) ──
if let Some((lba, efi_sectors)) = efi {
// Section header entry.
c[64] = 0x91; // final section header
c[65] = PLATFORM_EFI; // 0xEF
c[66..68].copy_from_slice(&1u16.to_le_bytes()); // one section entry
// Section entry.
c[96] = 0x88; // bootable
c[97] = 0x00; // no emulation
c[98..100].copy_from_slice(&0u16.to_le_bytes()); // load segment
c[100] = 0x00; // system type
c[102..104].copy_from_slice(&efi_sectors.to_le_bytes()); // sector count (512B units)
c[104..108].copy_from_slice(&lba.to_le_bytes()); // load RBA
}
Ok(c)
}
/// El-Torito validation-entry checksum: the 16-bit word sum of the 32-byte entry
/// (with the checksum field zero) must make the total zero mod 2^16.
fn validation_checksum(entry: &[u8]) -> u16 {
let mut sum: u16 = 0;
for pair in entry.chunks_exact(2) {
sum = sum.wrapping_add(u16::from_le_bytes([pair[0], pair[1]]));
}
0u16.wrapping_sub(sum)
}
/// Fill a fixed-width field with space-padded ISO d-characters (uppercase).
fn fill_dchars(field: &mut [u8], text: &str) {
for b in field.iter_mut() {
*b = b' ';
}
let up = text.to_ascii_uppercase();
for (i, ch) in up.bytes().enumerate() {
if i >= field.len() {
break;
}
field[i] = if is_dchar(ch) { ch } else { b'_' };
}
}
/// ISO-9660 d-characters: `A-Z`, `0-9`, `_`.
fn is_dchar(b: u8) -> bool {
b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_'
}
/// Normalize a filename to an ISO-9660 file identifier: uppercase d-chars, a
/// single `.` separator preserved, `;1` version suffix appended.
fn iso_file_name(name: &str) -> String {
let (stem, ext) = match name.rsplit_once('.') {
Some((s, e)) if !s.is_empty() => (s, Some(e)),
_ => (name, None),
};
let clean = |s: &str| -> String {
s.to_ascii_uppercase()
.bytes()
.map(|b| if is_dchar(b) { b as char } else { '_' })
.collect()
};
match ext {
Some(e) => format!("{}.{};1", clean(stem), clean(e)),
None => format!("{};1", clean(stem)),
}
}
/// Normalize a directory name to an ISO-9660 directory identifier (no version).
fn iso_dir_name(name: &str) -> String {
name.to_ascii_uppercase()
.bytes()
.map(|b| if is_dchar(b) || b == b'.' { b as char } else { '_' })
.collect()
}
/// Drop the trailing `;1` version from an ISO file identifier (for placement keys).
fn strip_version(name: &str) -> String {
name.rsplit_once(';').map(|(s, _)| s.to_string()).unwrap_or_else(|| name.to_string())
}
/// Normalize a caller-supplied ISO path (as used to mark a boot image) to the
/// uppercased, version-stripped, `/`-joined key form the placement map uses.
fn normalize_iso_path(p: &str) -> String {
p.split('/')
.filter(|c| !c.is_empty() && *c != ".")
.enumerate()
.map(|(_, c)| strip_version(&iso_file_name(c)))
.collect::<Vec<_>>()
.join("/")
}
/// Resolve a boot-image ISO path to its placement, erroring clearly if absent.
fn lookup_placement<'a>(
places: &'a BTreeMap<String, FilePlacement>,
iso_path: &str,
) -> Result<&'a FilePlacement> {
places.get(iso_path).with_context(|| {
format!(
"iso writer: boot image '{iso_path}' was not staged on the ISO (known: {:?})",
places.keys().collect::<Vec<_>>()
)
})
}
/// Write `data` starting at sector `lba` (data may span multiple sectors).
fn write_at(img: &mut [u8], lba: u32, data: &[u8]) {
let off = lba as usize * SECTOR;
img[off..off + data.len()].copy_from_slice(data);
}
/// Write a single ≤-sector blob at sector `lba`.
fn write_sector(img: &mut [u8], lba: u32, data: &[u8]) {
debug_assert!(data.len() <= SECTOR);
write_at(img, lba, data);
}
/// Pad a buffer up to a whole [`SECTOR`].
fn pad_to_sector(buf: &mut Vec<u8>) {
let pad = round_sectors(buf.len() as u64) as usize * SECTOR - buf.len();
buf.extend(std::iter::repeat_n(0u8, pad));
}
/// Read a little-endian u32 from `img` at absolute byte offset `off`.
#[cfg(test)]
fn read_u32_le(img: &[u8], off: usize) -> u32 {
u32::from_le_bytes([img[off], img[off + 1], img[off + 2], img[off + 3]])
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
/// The hand-assembled refusal stub IS what its listing says: every branch
/// target lands on the instruction the listing names, the message sits
/// exactly where `mov si` points, and the sector carries the `55 AA`
/// signature the hybrid MBR needs. Offsets asserted here so an edit to the
/// byte table cannot silently turn a `jz H` into a jump into the message.
#[test]
fn the_bios_refusal_sector_is_a_valid_boot_sector() {
let s = bios_refusal_sector();
assert_eq!(s.len(), 512);
assert_eq!(s[0], 0xFA, "cli first");
// mov si, imm16 → the message address, 0x7C00-relative.
assert_eq!(s[0x0B], 0xBE);
let msg_at = u16::from_le_bytes([s[0x0C], s[0x0D]]) as usize - 0x7C00;
assert_eq!(msg_at, 0x24, "message right after the code");
assert!(s[msg_at..].starts_with(BIOS_REFUSAL_TEXT.as_bytes()));
let end = msg_at + BIOS_REFUSAL_TEXT.len();
assert_eq!(&s[end..end + 3], b"\r\n\0", "CRLF + NUL terminator");
// jz H: from 0x13, rel 0x0D → 0x20, which must be `cli; hlt`.
assert_eq!(s[0x11], 0x74);
let h = 0x13 + s[0x12] as usize;
assert_eq!((s[h], s[h + 1]), (0xFA, 0xF4), "H: cli; hlt");
// jmp L: from 0x20, rel 0xEE (−18) → 0x0E, which must be `lodsb`.
assert_eq!(s[0x1E], 0xEB);
let l = (0x20i32 + (s[0x1F] as i8) as i32) as usize;
assert_eq!(s[l], 0xAC, "L: lodsb");
// jmp H: from 0x24, rel 0xFC (−4) → 0x20.
assert_eq!(s[0x22], 0xEB);
assert_eq!((0x24i32 + (s[0x23] as i8) as i32) as usize, h);
// the serial mirror writes to COM1 before the INT 10h teletype call.
assert_eq!(&s[0x13..0x17], &[0xBA, 0xF8, 0x03, 0xEE], "mov dx,0x3F8; out dx,al");
assert_eq!(&s[0x1C..0x1E], &[0xCD, 0x10], "int 0x10");
assert_eq!((s[510], s[511]), (0x55, 0xAA));
}
/// A UEFI-only medium's El-Torito DEFAULT (80x86) entry no longer points at
/// the FAT ESP: it loads the one-sector refusal stub, while the EFI section
/// still points at the ESP. RED before 2026-08-28: the default entry's RBA
/// equalled the EFI section's, and SeaBIOS executed FAT bytes.
#[test]
fn a_uefi_only_medium_carries_the_bios_refusal_as_its_default_entry() {
let dir = tempfile::tempdir().unwrap();
let efi = dir.path().join("efiboot.img");
std::fs::write(&efi, vec![0xEFu8; 8192]).unwrap();
let mut w = IsoWriter::new("UEFI_ONLY");
w.add_file("efiboot.img", &efi).unwrap();
w.set_efi_boot("efiboot.img");
let img = w.build_image().unwrap();
let cat = 21 * SECTOR;
let def_lba = read_u32_le(&img, cat + 40) as usize;
let def_sectors = u16::from_le_bytes([img[cat + 38], img[cat + 39]]);
let efi_lba = read_u32_le(&img, cat + 104) as usize;
assert_ne!(def_lba, efi_lba, "the default entry must NOT be the ESP");
assert_eq!(def_sectors, 1, "one 512-byte sector: the stub");
let stub = &img[def_lba * SECTOR..def_lba * SECTOR + 512];
assert_eq!(stub, &bios_refusal_sector()[..], "the default entry loads the refusal stub");
assert_eq!(&img[efi_lba * SECTOR..efi_lba * SECTOR + 4], &[0xEF; 4], "EFI section still the ESP");
// It is a real file on the volume too, so `isoinfo -l` shows what the
// BIOS would run, and the receipt names it.
let layout = w.layout().unwrap();
assert!(layout.bios_refusal);
assert!(layout.describe().contains(BIOS_REFUSAL_FILE), "{}", layout.describe());
}
/// A medium that DECLARES a BIOS boot image keeps it: the refusal stub is
/// only ever laid where nothing else would answer a BIOS.
#[test]
fn a_declared_bios_image_is_not_replaced_by_the_refusal() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("bzImage"), vec![0xB2u8; 4096]).unwrap();
std::fs::write(dir.path().join("efiboot.img"), vec![0xEFu8; 4096]).unwrap();
let mut w = IsoWriter::new("BOTH");
w.add_file("bzImage", dir.path().join("bzImage")).unwrap();
w.add_file("efiboot.img", dir.path().join("efiboot.img")).unwrap();
w.set_bios_boot("bzImage", 4);
w.set_efi_boot("efiboot.img");
let img = w.build_image().unwrap();
let cat = 21 * SECTOR;
let def_lba = read_u32_le(&img, cat + 40) as usize;
assert_eq!(&img[def_lba * SECTOR..def_lba * SECTOR + 4], &[0xB2; 4], "the declared kernel");
assert!(!w.layout().unwrap().bios_refusal);
assert!(!w.layout().unwrap().describe().contains(BIOS_REFUSAL_FILE));
}
/// **A declared BIOS stage is the default entry, sized to the whole image,
/// and its patch table comes back off the mastered medium naming the
/// kernel's and the initramfs' real LBAs** — and no refusal stub is laid
/// beside it (a medium with a real BIOS path has nothing to refuse).
#[test]
fn a_bios_stage_is_patched_with_the_payload_placements_and_is_the_default_entry() {
let dir = tempfile::tempdir().unwrap();
// A "stage": 0x400 bytes of code-shaped filler, the blank table, a tail.
let mut stage = vec![0x90u8; 0x400];
stage.extend_from_slice(&crate::bios_patch::blank_table());
stage.extend_from_slice(b"korp-boot-bios strings\0");
std::fs::write(dir.path().join("stage.bin"), &stage).unwrap();
std::fs::write(dir.path().join("vmlinuz"), vec![0x4Bu8; 5000]).unwrap();
std::fs::write(dir.path().join("initrd"), vec![0x49u8; 3000]).unwrap();
std::fs::write(dir.path().join("efiboot.img"), vec![0xEFu8; 4096]).unwrap();
let mut w = IsoWriter::new("BIOSUEFI");
w.add_file("bios/stage.bin", dir.path().join("stage.bin")).unwrap();
w.add_file("bios/initrd", dir.path().join("initrd")).unwrap();
w.add_file("vmlinuz", dir.path().join("vmlinuz")).unwrap();
w.add_file("efiboot.img", dir.path().join("efiboot.img")).unwrap();
w.set_efi_boot("efiboot.img");
w.set_bios_stage(BiosStage {
image: "bios/stage.bin".into(),
kernel: "vmlinuz".into(),
initrd: Some("bios/initrd".into()),
cmdline: "console=ttyS0 korp.entry=gunnar".into(),
});
let img = w.build_image().unwrap();
let layout = *w.layout().unwrap();
let b = layout.bios.expect("a BIOS stage placement is recorded");
assert!(!layout.bios_refusal, "a real stage means no refusal stub");
assert!(!layout.describe().contains(BIOS_REFUSAL_FILE));
assert!(layout.describe().contains("legacy BIOS: real stage"));
// The catalog's default entry: the stage's LBA and the WHOLE image in
// 512-byte sectors (stage is 0x400 + 0x420 + 23 = 2107 B → 5 sectors).
let cat = 21 * SECTOR;
assert_eq!(read_u32_le(&img, cat + 40), b.lba);
assert_eq!(u16::from_le_bytes([img[cat + 38], img[cat + 39]]), b.catalog_sectors);
assert_eq!(b.catalog_sectors, (stage.len() as u16).div_ceil(512));
// The EFI section still points at the ESP — the UEFI path is untouched.
let efi = layout.efi.unwrap();
assert_eq!(read_u32_le(&img, cat + 104), efi.lba);
assert_eq!(&img[efi.lba as usize * SECTOR..efi.lba as usize * SECTOR + 4], &[0xEF; 4]);
// The patch table on the MEDIUM names where the payloads really are.
let on_medium = &img[b.lba as usize * SECTOR..b.lba as usize * SECTOR + stage.len()];
let off = crate::bios_patch::find_table(on_medium).unwrap();
let t = crate::bios_patch::BiosPatchTable::read_from(on_medium, off).unwrap();
assert_eq!(t.cmdline, "console=ttyS0 korp.entry=gunnar");
assert_eq!((t.kernel_lba, t.kernel_len), (b.kernel_lba, 5000));
assert_eq!((t.initrd_lba, t.initrd_len), (b.initrd_lba, 3000));
assert_eq!(&img[t.kernel_lba as usize * SECTOR..t.kernel_lba as usize * SECTOR + 4], &[0x4B; 4]);
assert_eq!(&img[t.initrd_lba as usize * SECTOR..t.initrd_lba as usize * SECTOR + 4], &[0x49; 4]);
// The refusal file is not on the volume at all.
assert!(!w.nodes[0].files.contains_key(&iso_file_name(BIOS_REFUSAL_FILE)));
// Deterministic: a second build is byte-identical (the patch is idempotent).
assert_eq!(w.build_image().unwrap(), img);
}
/// **The hybrid boot sector, and the two things it must be true about.**
///
/// The RED this replaces was MEASURED on gunnar's real 38 680 576 B medium
/// (2026-08-30, lane AQ): `LBA 0[510..512] = 00 00`, the whole first sector
/// zero, so SeaBIOS booting the identical bytes off a hard-disk device
/// found no boot record and parked — `--target ovh-public-cloud` left ZERO
/// bytes of serial for 420 s. This asserts APPLIED OUTPUT on both halves:
/// the boot sector a firmware reads, and the disk-flavoured table the code
/// it runs will read.
///
/// The first assertion is the RED itself and it stays: the SAME tree
/// without the word still has an all-zero LBA 0. A hybrid that could not be
/// told from a plain medium would be no hybrid.
#[test]
fn a_hybrid_medium_has_a_boot_sector_and_a_disk_flavoured_stage_and_costs_no_bytes() {
let dir = tempfile::tempdir().unwrap();
let mut stage = vec![0x90u8; 0x400];
stage.extend_from_slice(&crate::bios_patch::blank_table());
stage.extend_from_slice(b"korp-boot-bios strings\0");
std::fs::write(dir.path().join("stage.bin"), &stage).unwrap();
std::fs::write(dir.path().join("vmlinuz"), vec![0x4Bu8; 5000]).unwrap();
std::fs::write(dir.path().join("initrd"), vec![0x49u8; 3000]).unwrap();
std::fs::write(dir.path().join("efiboot.img"), vec![0xEFu8; 4096]).unwrap();
let tree = |w: &mut IsoWriter| {
w.add_file("bios/stage.bin", dir.path().join("stage.bin")).unwrap();
w.add_file("bios/initrd", dir.path().join("initrd")).unwrap();
w.add_file("vmlinuz", dir.path().join("vmlinuz")).unwrap();
w.add_file("efiboot.img", dir.path().join("efiboot.img")).unwrap();
w.set_efi_boot("efiboot.img");
w.set_bios_stage(BiosStage {
image: "bios/stage.bin".into(),
kernel: "vmlinuz".into(),
initrd: Some("bios/initrd".into()),
cmdline: "console=ttyS0 korp.entry=gunnar".into(),
});
};
// ── RED: the same tree with no hybrid word ──────────────────────────
let mut plain = IsoWriter::new("HYBRIDT");
tree(&mut plain);
let plain_img = plain.build_image().unwrap();
assert_eq!(&plain_img[510..512], &[0x00, 0x00], "a plain medium has NO boot sector");
assert!(plain_img[..512].iter().all(|b| *b == 0), "its whole LBA 0 is zero");
assert!(plain.layout().unwrap().hybrid_mbr.is_none());
// ── GREEN: the word, and nothing else, changes ──────────────────────
// The code is a stand-in for `korp_boot_bios::emit_mbr`'s (this crate
// cannot depend on the assembler); what is asserted is that the writer
// lays exactly the bytes it was handed, at LBA 0, with a partition
// table and the signature after them.
let code: Vec<u8> = (0..200u32).map(|i| (i % 251) as u8).collect();
let mut w = IsoWriter::new("HYBRIDT");
tree(&mut w);
w.set_hybrid_mbr(code.clone());
let img = w.build_image().unwrap();
let layout = *w.layout().unwrap();
let h = layout.hybrid_mbr.expect("a hybrid placement is recorded");
// 1. THE BOOT SECTOR a firmware reads off a hard-disk device.
assert_eq!(&img[..code.len()], &code[..], "the code the caller assembled, at LBA 0");
assert_eq!((img[510], img[511]), (0x55, 0xAA), "the signature that was 00 00");
assert_eq!(img[446], 0x80, "one bootable partition");
assert_eq!(img[446 + 4], 0x83);
assert_eq!(read_u32_le(&img, 446 + 8), 0, "spanning the medium from LBA 0");
assert_eq!(read_u32_le(&img, 446 + 12), (img.len() / 512) as u32);
// 2. THE STAGE the code chainloads: the same image, a 512-byte table.
assert_eq!(h.stage_lba, HYBRID_STAGE_LBA);
assert_eq!(h.stage_sectors as usize, stage.len().div_ceil(512), "rounded UP: a truncated read leaves the stage's tail unloaded");
let at = HYBRID_STAGE_LBA as usize * 512;
let twin = &img[at..at + stage.len()];
assert_eq!(&twin[..0x400], &stage[..0x400], "the SAME stage image, not a second loader");
let off = crate::bios_patch::find_table(twin).unwrap();
let t = crate::bios_patch::BiosPatchTable::read_from(twin, off).unwrap();
assert_eq!(t.sector_shift, crate::bios_patch::SECTOR_SHIFT_DISK, "512-byte sectors");
assert_eq!(t.cmdline, "console=ttyS0 korp.entry=gunnar");
let cd = layout.bios.unwrap();
assert_eq!(t.kernel_lba, cd.kernel_lba * 4, "the SAME bytes, counted in 512s");
assert_eq!(t.initrd_lba, cd.initrd_lba * 4);
// and those LBAs really land on the payloads (the whole point).
assert_eq!(&img[t.kernel_lba as usize * 512..t.kernel_lba as usize * 512 + 4], &[0x4B; 4]);
assert_eq!(&img[t.initrd_lba as usize * 512..t.initrd_lba as usize * 512 + 4], &[0x49; 4]);
// The El-Torito copy is UNTOUCHED: the cdrom path still reads 2048s.
let cd_at = cd.lba as usize * SECTOR;
let cdt = crate::bios_patch::BiosPatchTable::read_from(
&img[cd_at..cd_at + stage.len()],
crate::bios_patch::find_table(&img[cd_at..cd_at + stage.len()]).unwrap(),
)
.unwrap();
assert_eq!(cdt.sector_shift, crate::bios_patch::SECTOR_SHIFT_CD);
// 3. IT COSTS NO BYTES. Both copies and the boot sector live in the
// ISO's reserved system area, so the medium is the same size and
// every byte from the PVD on is identical to the plain one.
assert_eq!(img.len(), plain_img.len(), "a hybrid medium is not one byte bigger");
assert_eq!(&img[SYSTEM_AREA_BYTES..], &plain_img[SYSTEM_AREA_BYTES..], "only the system area moved");
assert!(layout.describe().contains("HYBRID"), "{}", layout.describe());
assert_eq!(w.build_image().unwrap(), img, "deterministic");
}
/// A `55 AA` that chainloads nothing is WORSE than no boot sector —
/// firmware stops looking the moment it finds one — so a hybrid medium
/// with no real BIOS stage is refused by name rather than mastered.
#[test]
fn a_hybrid_medium_without_a_bios_stage_is_refused_by_name() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("efiboot.img"), vec![0xEFu8; 4096]).unwrap();
let mut w = IsoWriter::new("NOSTAGE");
w.add_file("efiboot.img", dir.path().join("efiboot.img")).unwrap();
w.set_efi_boot("efiboot.img");
w.set_hybrid_mbr(vec![0x90; 8]);
let e = w.build_image().unwrap_err().to_string();
assert!(e.contains("no real legacy-BIOS stage"), "{e}");
assert!(e.contains("55 AA"), "{e}");
}
/// A stage without a patch table, or a stage whose kernel is not on the
/// volume, stops the build by name — never a medium that boots into LBA 0.
#[test]
fn a_bios_stage_without_a_table_or_kernel_is_refused() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("stage.bin"), vec![0x90u8; 1024]).unwrap();
std::fs::write(dir.path().join("efiboot.img"), vec![0xEFu8; 4096]).unwrap();
let mut w = IsoWriter::new("BAD");
w.add_file("bios/stage.bin", dir.path().join("stage.bin")).unwrap();
w.add_file("efiboot.img", dir.path().join("efiboot.img")).unwrap();
w.set_efi_boot("efiboot.img");
w.set_bios_stage(BiosStage {
image: "bios/stage.bin".into(),
kernel: "vmlinuz".into(),
initrd: None,
cmdline: "x".into(),
});
let err = w.build_image().unwrap_err().to_string();
assert!(err.contains("vmlinuz") || err.contains("VMLINUZ"), "{err}");
std::fs::write(dir.path().join("vmlinuz"), vec![0x4Bu8; 5000]).unwrap();
w.add_file("vmlinuz", dir.path().join("vmlinuz")).unwrap();
let err = format!("{:#}", w.build_image().unwrap_err());
assert!(err.contains("no patch table"), "{err}");
}
/// Build a tiny ISO in memory and assert the ISO-9660 on-disk structures the
/// spec mandates: the `CD001` PVD magic at sector 16, the El-Torito boot
/// record + validation entry, and that a staged payload file is placed and
/// readable back at its recorded extent.
#[test]
fn masters_iso9660_structures_and_reads_payload_back() {
let dir = tempfile::tempdir().unwrap();
// Two payload files, one nested — proving the directory tree + placement.
let kernel = dir.path().join("bzImage");
std::fs::write(&kernel, b"KERNEL-PAYLOAD-BYTES").unwrap();
let efi = dir.path().join("efiboot.img");
let efi_bytes = vec![0xEFu8; 4096];
std::fs::write(&efi, &efi_bytes).unwrap();
let nested = dir.path().join("readme.txt");
std::fs::write(&nested, b"hello from the iso").unwrap();
let mut w = IsoWriter::new("KORP_DEMO");
w.add_file("bzImage", &kernel).unwrap();
w.add_file("efiboot.img", &efi).unwrap();
w.add_file("docs/readme.txt", &nested).unwrap();
w.set_bios_boot("bzImage", 4);
w.set_efi_boot("efiboot.img");
let img = w.build_image().unwrap();
// ── PVD: "CD001" magic at sector 16, type 1 ──
assert_eq!(&img[16 * SECTOR + 1..16 * SECTOR + 6], b"CD001", "PVD magic");
assert_eq!(img[16 * SECTOR], 1, "PVD type == 1");
// volume id (uppercased, space-padded).
let volid = &img[16 * SECTOR + 40..16 * SECTOR + 72];
assert!(volid.starts_with(b"KORP_DEMO"), "volume id: {:?}", volid);
// logical block size == 2048 (both-endian LE half).
assert_eq!(read_u32_le(&img, 16 * SECTOR + 128) & 0xFFFF, 2048, "logical block size");
// ── El-Torito boot record VD at sector 17 ──
assert_eq!(img[17 * SECTOR], 0, "boot record type == 0");
assert_eq!(&img[17 * SECTOR + 1..17 * SECTOR + 6], b"CD001");
assert!(
img[17 * SECTOR + 7..17 * SECTOR + 30].starts_with(b"EL TORITO SPECIFICATION"),
"El-Torito boot-system id"
);
let cat_lba = read_u32_le(&img, 17 * SECTOR + 71);
assert_eq!(cat_lba, 21, "boot-catalog pointer");
// ── Volume Descriptor Set Terminator at sector 18 ──
assert_eq!(img[18 * SECTOR], 255, "terminator type == 255");
assert_eq!(&img[18 * SECTOR + 1..18 * SECTOR + 6], b"CD001");
// ── Boot catalog: validation entry + EFI section ──
let cat = 21 * SECTOR;
assert_eq!(img[cat], 0x01, "validation entry header id");
assert_eq!(img[cat + 30], 0x55, "validation key 0x55");
assert_eq!(img[cat + 31], 0xAA, "validation key 0xAA");
// the 16-bit words of the validation entry sum to zero.
let mut sum: u16 = 0;
for pair in img[cat..cat + 32].chunks_exact(2) {
sum = sum.wrapping_add(u16::from_le_bytes([pair[0], pair[1]]));
}
assert_eq!(sum, 0, "validation entry checksum");
// default/initial entry bootable.
assert_eq!(img[cat + 32], 0x88, "default entry bootable");
// EFI section header (platform 0xEF).
assert_eq!(img[cat + 64], 0x91, "final section header");
assert_eq!(img[cat + 65], PLATFORM_EFI, "EFI platform id");
assert_eq!(img[cat + 96], 0x88, "EFI section entry bootable");
let efi_lba = read_u32_le(&img, cat + 104);
// ── the EFI boot image is placed at the catalog's recorded extent ──
let efi_off = efi_lba as usize * SECTOR;
assert_eq!(&img[efi_off..efi_off + efi_bytes.len()], &efi_bytes[..], "efiboot.img readable back at its El-Torito extent");
// ── the total image is sector-aligned ──
assert_eq!(img.len() % SECTOR, 0, "image is a whole number of sectors");
assert!(img.len() >= 22 * SECTOR, "past the fixed descriptors");
}
/// Mastering the same tree twice yields byte-identical images (deterministic,
/// the property content-addressing needs).
#[test]
fn mastering_is_deterministic() {
let dir = tempfile::tempdir().unwrap();
let mut f = std::fs::File::create(dir.path().join("payload.bin")).unwrap();
f.write_all(&[0xABu8; 3000]).unwrap();
drop(f);
let build = || {
let mut w = IsoWriter::new("DET_TEST");
w.add_file("payload.bin", dir.path().join("payload.bin")).unwrap();
w.build_image().unwrap()
};
assert_eq!(build(), build(), "same input tree → byte-identical ISO");
}
/// **The silent El-Torito clamp.** A boot image one byte over the format's
/// 16-bit sector count must stop the build, naming the numbers — not be cut
/// down to fit.
///
/// Asserted on the **catalog bytes**, which are the applied output. The old
/// code's `.min(u16::MAX)` produced a perfectly well-formed catalog whose
/// sector count said `65535` for an image that was larger, so every
/// structural assertion in this file passed and the ISO mastered clean. The
/// failure surfaced on real firmware as `no bootable device`, with nothing
/// in the build output to search for.
///
/// Both sides of the boundary, because a check that only ever sees the
/// oversized case cannot tell a working limit from a rejection of
/// everything.
#[test]
fn an_oversized_boot_image_is_an_error_naming_the_numbers_not_a_silent_clamp() {
// ── exactly at the ceiling: described, and described EXACTLY ──
let at_ceiling_sectors = el_torito_sectors(MAX_BOOT_IMAGE_BYTES, "EFI (efiboot.img)")
.expect("an image of exactly 33 553 920 B is representable");
let images = BootImages::new(None, Some((40, at_ceiling_sectors)))
.expect("an EFI image is a boot image");
let at_ceiling = boot_catalog(&images)
.expect("an image of exactly 33 553 920 B is representable and must be accepted");
assert_eq!(
u16::from_le_bytes([at_ceiling[102], at_ceiling[103]]),
u16::MAX,
"an image at the ceiling must fill the sector count exactly; anything less means the \
boundary is off by one and real images are being described short"
);
assert_eq!(
el_torito_sectors(MAX_BOOT_IMAGE_BYTES, "efi").unwrap(),
65535
);
assert_eq!(el_torito_sectors(0, "efi").unwrap(), 1, "zero reads as `no image` to firmware");
assert_eq!(el_torito_sectors(1, "efi").unwrap(), 1, "rounds UP: a partial sector still ships");
assert_eq!(el_torito_sectors(513, "efi").unwrap(), 2);
// ── one byte over: refused, loudly ──
let over = MAX_BOOT_IMAGE_BYTES + 1;
let err = el_torito_sectors(over, "EFI (efiboot.img)")
.expect_err("one byte past a 16-bit field cannot be described and must not be clamped");
let msg = format!("{err:#}");
for needle in [
&over.to_string(), // the actual size
&MAX_BOOT_IMAGE_BYTES.to_string(), // the ceiling
"1 bytes", // the overflow
"EFI (efiboot.img)", // which image
] {
assert!(
msg.contains(needle),
"the error must name {needle:?} — an operator who is told only `too large` has to \
go and measure the file, the ceiling and the difference themselves. Got: {msg}"
);
}
// ── and the real gunnar-shaped margin, so the numbers in the doc are
// the numbers the code enforces ──
const GUNNAR_ESP_2026_08_03: u64 = 33_471_488;
assert!(
el_torito_sectors(GUNNAR_ESP_2026_08_03, "efi").is_ok(),
"gunnar's shipping ESP must still master"
);
assert_eq!(
MAX_BOOT_IMAGE_BYTES - GUNNAR_ESP_2026_08_03,
82_432,
"the margin the ceiling leaves gunnar's appliance ESP is 82 432 B (~82 KiB). If this \
assertion is what broke, the ceiling arithmetic moved, not the ESP"
);
}
/// The same refusal through the **real** mastering path, not just the
/// catalog helper, so the error cannot be one that nothing propagates.
#[test]
fn build_image_refuses_an_oversized_efi_boot_image() {
let dir = tempfile::tempdir().unwrap();
let big = dir.path().join("efiboot.img");
// One virtual sector past what the 16-bit count can address.
let len = MAX_BOOT_IMAGE_BYTES + EL_TORITO_VIRTUAL_SECTOR;
let f = std::fs::File::create(&big).unwrap();
f.set_len(len).unwrap();
drop(f);
let mut w = IsoWriter::new("TOO_BIG");
w.add_file("efiboot.img", &big).unwrap();
w.set_efi_boot("efiboot.img");
let err = w
// `map(len)` before `expect_err`: on a red the Ok side is the whole
// 33 MB image, and `expect_err` would print every byte of it. The
// first run of this test emitted 96 MB of decimal zeros, which is a
// failure report nobody can read.
.build_image()
.map(|img| img.len())
.expect_err("an oversized ESP must fail the BUILD, where a human is watching, rather \
than the boot, where nobody is");
let msg = format!("{err:#}");
assert!(
msg.contains(&len.to_string()) && msg.contains(&MAX_BOOT_IMAGE_BYTES.to_string()),
"the propagated error lost the numbers: {msg}"
);
assert!(
msg.contains("efiboot.img"),
"the propagated error must name the image: {msg}"
);
// …and an ordinary ISO still masters, so this is a limit and not a
// blanket refusal.
let small = dir.path().join("small.img");
std::fs::write(&small, vec![0xEFu8; 4096]).unwrap();
let mut ok = IsoWriter::new("FINE");
ok.add_file("efiboot.img", &small).unwrap();
ok.set_efi_boot("efiboot.img");
let img = ok.build_image().expect("a normal ESP still masters");
assert_eq!(
u16::from_le_bytes([img[21 * SECTOR + 102], img[21 * SECTOR + 103]]),
8,
"4096 B is 8 virtual sectors of 512 B"
);
}
/// **An ISO with no boot image is not presented as bootable.**
///
/// The catalog builder used to have a third arm:
///
/// ```text
/// (None, None) => (0, 4), // no boot image at all
/// c[32] = 0x88; // bootable
/// c[33] = 0x00; // no emulation
/// ```
///
/// so an image carrying nothing bootable still got an El-Torito Boot Record
/// volume descriptor, a checksummed validation entry, and a default entry
/// marked **bootable, no-emulation, load RBA 0** — firmware pointed at the
/// ISO's own system area. That is a lie told in the exact format firmware
/// trusts.
///
/// It also made every structural assertion in this file's bootable tests
/// unfalsifiable: `CD001`, the boot-record type, the El-Torito id, the
/// validation header and the `0x55AA` key were emitted unconditionally, so
/// they passed for an image with no boot image and would pass for one whose
/// boot image never resolved.
///
/// The writer now emits a plain ISO-9660 data volume when nothing is
/// bootable — which is also what `mkisofs` does when the argv carries no
/// `-b` and no `-e`, and what `IsoBuild::master_args` renders in that case.
#[test]
fn an_iso_with_no_boot_image_is_not_presented_as_bootable() {
let dir = tempfile::tempdir().unwrap();
let data = dir.path().join("basemap.bin");
std::fs::write(&data, vec![0x5Au8; 5000]).unwrap();
let mut w = IsoWriter::new("KORP_DATA");
w.add_file("basemap.bin", &data).unwrap();
// …and NOTHING is marked bootable.
let img = w.build_image().expect("a data-only ISO is legitimate and must still master");
// It is a real ISO-9660 volume: the PVD is still there.
assert_eq!(&img[16 * SECTOR + 1..16 * SECTOR + 6], b"CD001", "PVD magic");
assert_eq!(img[16 * SECTOR], 1, "PVD type");
// …and the volume descriptor sequence goes straight to the terminator,
// with no Boot Record VD wedged in front of it.
assert_eq!(
img[17 * SECTOR], 255,
"with nothing bootable, sector 17 must be the Volume Descriptor Set Terminator, not \
an El-Torito Boot Record announcing a catalog for boot images that do not exist"
);
assert_eq!(&img[17 * SECTOR + 1..17 * SECTOR + 6], b"CD001", "terminator standard id");
// Nowhere in the image does it claim El-Torito.
assert!(
!img.windows(23).any(|w| w == b"EL TORITO SPECIFICATION"),
"a non-bootable ISO must not carry an El-Torito boot-system id anywhere"
);
// …and no 32-byte entry anywhere is a validation entry with the 0x55AA key
// followed by a bootable default entry.
for s in 0..img.len() / SECTOR {
let c = s * SECTOR;
assert!(
!(img[c] == 0x01 && img[c + 30] == 0x55 && img[c + 31] == 0xAA),
"sector {s} holds an El-Torito validation entry in an ISO with no boot image"
);
}
// The data itself is still on the volume, so this is a non-bootable ISO
// and not an empty one.
assert!(
img.windows(16).any(|w| w == [0x5Au8; 16]),
"the payload must still be mastered onto the data volume"
);
// ── GREEN control ──────────────────────────────────────────────────
// The SAME tree with one file marked bootable does get every structure
// above, so the assertions are about the absence of a boot image and not
// about the writer having stopped emitting El-Torito altogether.
let esp = dir.path().join("efiboot.img");
std::fs::write(&esp, vec![0xEFu8; 8192]).unwrap();
let mut w = IsoWriter::new("KORP_DATA");
w.add_file("basemap.bin", &data).unwrap();
w.add_file("efiboot.img", &esp).unwrap();
w.set_efi_boot("efiboot.img");
let img = w.build_image().expect("a bootable ISO still masters");
assert_eq!(img[17 * SECTOR], 0, "boot record VD is back");
assert!(
img[17 * SECTOR + 7..17 * SECTOR + 30].starts_with(b"EL TORITO SPECIFICATION"),
"…with the El-Torito id"
);
let cat = read_u32_le(&img, 17 * SECTOR + 71) as usize * SECTOR;
assert_eq!(img[cat], 0x01);
assert_eq!(img[cat + 30], 0x55);
assert_eq!(img[cat + 31], 0xAA);
assert_eq!(img[cat + 32], 0x88, "default entry bootable");
}
// ── the GPT hybrid ─────────────────────────────────────────────────────
/// An independent GPT reader for the tests: header magic + both CRC32s
/// checked, the first entry parsed. Deliberately NOT the writer's own
/// serializers read backwards — a reader that shares the writer's arithmetic
/// agrees with it by construction and proves nothing.
struct ParsedGpt {
my_lba: u64,
alt_lba: u64,
first_usable: u64,
last_usable: u64,
entries_lba: u64,
disk_guid: [u8; 16],
type_guid: [u8; 16],
part_guid: [u8; 16],
part_first: u64,
part_last: u64,
part_name: String,
}
fn parse_gpt_at(img: &[u8], header_lba: u64) -> ParsedGpt {
let h = &img[(header_lba * GPT_LBA) as usize..][..92];
assert_eq!(&h[0..8], b"EFI PART", "GPT signature at LBA {header_lba}");
assert_eq!(u32::from_le_bytes(h[12..16].try_into().unwrap()), 92, "header size");
let recorded_crc = u32::from_le_bytes(h[16..20].try_into().unwrap());
let mut zeroed = h.to_vec();
zeroed[16..20].fill(0);
assert_eq!(crc32fast::hash(&zeroed), recorded_crc, "header CRC32 at LBA {header_lba}");
let u64_at = |o: usize| u64::from_le_bytes(h[o..o + 8].try_into().unwrap());
let entries_lba = u64_at(72);
let n = u32::from_le_bytes(h[80..84].try_into().unwrap()) as usize;
let sz = u32::from_le_bytes(h[84..88].try_into().unwrap()) as usize;
let array = &img[(entries_lba * GPT_LBA) as usize..][..n * sz];
assert_eq!(
crc32fast::hash(array),
u32::from_le_bytes(h[88..92].try_into().unwrap()),
"partition array CRC32 (header at LBA {header_lba})"
);
let e = &array[..sz];
let mut disk_guid = [0u8; 16];
disk_guid.copy_from_slice(&h[56..72]);
let mut type_guid = [0u8; 16];
type_guid.copy_from_slice(&e[0..16]);
let mut part_guid = [0u8; 16];
part_guid.copy_from_slice(&e[16..32]);
let name_units: Vec<u16> = e[56..128]
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.take_while(|&u| u != 0)
.collect();
// Every OTHER entry is empty — one partition, nothing else claimed.
for (i, other) in array.chunks_exact(sz).enumerate().skip(1) {
assert!(other.iter().all(|&b| b == 0), "entry {i} must be unused");
}
ParsedGpt {
my_lba: u64_at(24),
alt_lba: u64_at(32),
first_usable: u64_at(40),
last_usable: u64_at(48),
entries_lba,
disk_guid,
type_guid,
part_guid,
part_first: u64::from_le_bytes(e[32..40].try_into().unwrap()),
part_last: u64::from_le_bytes(e[40..48].try_into().unwrap()),
part_name: String::from_utf16(&name_units).unwrap(),
}
}
/// A real FAT image of `bytes` carrying one marker file, formatted with the
/// same crate the composers format the ESP with.
fn fat_esp(bytes: u64, marker: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; bytes as usize];
{
let mut cur = std::io::Cursor::new(&mut buf[..]);
fatfs::format_volume(&mut cur, fatfs::FormatVolumeOptions::new()).unwrap();
let fs = fatfs::FileSystem::new(&mut cur, fatfs::FsOptions::new()).unwrap();
let mut f = fs.root_dir().create_file("MARKER.BIN").unwrap();
f.write_all(marker).unwrap();
}
buf
}
/// **An ESP past El-Torito's ceiling masters as a GPT hybrid whose ESP
/// partition IS the FAT image** — parsed back with an independent reader,
/// the partition's byte range compared to the staged bytes, and the FAT
/// mounted through that range to read the marker file; the El-Torito entry
/// is clamped AND the layout says so.
///
/// RED first: with `GptHybrid::Off` the same tree is refused (the 2026-08-03
/// clamp lesson still holds for a plain medium), so the hybrid arm is what
/// makes this master, not a writer that stopped checking.
#[test]
fn the_hybrid_gpt_parses_back_and_spans_exactly_the_esp() {
let dir = tempfile::tempdir().unwrap();
// 34 MiB: 1 029 632 B past the ceiling — big enough to need the clamp,
// small enough for a unit test.
let esp_len = MAX_BOOT_IMAGE_BYTES + 1_029_632;
assert_eq!(esp_len % GPT_LBA, 0, "a FAT image is whole 512-byte sectors");
let marker = b"the-whole-fat-rides-the-gpt-partition";
let esp = fat_esp(esp_len, marker);
std::fs::write(dir.path().join("efiboot.img"), &esp).unwrap();
std::fs::write(dir.path().join("payload.bin"), vec![0x5Au8; 3000]).unwrap();
// ── RED: the plain writer refuses this ESP ──
let mut plain = IsoWriter::new("KORP_APPLIANCE");
plain.add_file("efiboot.img", dir.path().join("efiboot.img")).unwrap();
plain.set_efi_boot("efiboot.img");
let err = plain.build_image().map(|i| i.len()).expect_err("a plain medium must refuse");
assert!(format!("{err:#}").contains(&MAX_BOOT_IMAGE_BYTES.to_string()));
// ── GREEN: the hybrid arm ──
let mut w = IsoWriter::new("KORP_APPLIANCE");
w.add_file("efiboot.img", dir.path().join("efiboot.img")).unwrap();
w.add_file("payload.bin", dir.path().join("payload.bin")).unwrap();
w.set_efi_boot("efiboot.img");
w.set_gpt_hybrid(GptHybrid::WhenEspExceedsElTorito);
let img = w.build_image().expect("an oversized ESP masters as a hybrid");
let layout = *w.layout().expect("a build records its layout");
let efi = layout.efi.expect("an EFI entry");
let gpt = layout.gpt.expect("a GPT was laid");
// The image is whole ISO sectors AND whole GPT LBAs; the PVD's volume
// space covers all of it, tail included.
assert_eq!(img.len() % SECTOR, 0);
assert_eq!(img.len() as u64, layout.total_bytes);
assert_eq!(read_u32_le(&img, 16 * SECTOR + 80) as usize, img.len() / SECTOR, "PVD volume space");
// ── protective MBR ──
assert_eq!(&img[510..512], &[0x55, 0xAA], "MBR signature");
assert_eq!(img[446 + 4], 0xEE, "one protective (0xEE) partition");
assert_eq!(u32::from_le_bytes(img[454..458].try_into().unwrap()), 1, "protective starts at LBA 1");
assert_eq!(
u32::from_le_bytes(img[458..462].try_into().unwrap()) as u64,
img.len() as u64 / GPT_LBA - 1,
"protective spans the medium"
);
// ── primary + backup GPT, read back independently ──
let disk_last = img.len() as u64 / GPT_LBA - 1;
let primary = parse_gpt_at(&img, 1);
let backup = parse_gpt_at(&img, disk_last);
assert_eq!((primary.my_lba, primary.alt_lba), (1, disk_last));
assert_eq!((backup.my_lba, backup.alt_lba), (disk_last, 1));
assert_eq!(primary.entries_lba, 2);
assert_eq!(backup.entries_lba, disk_last - 32, "backup array sits right under the backup header");
assert_eq!(primary.first_usable, 34);
assert_eq!(primary.last_usable, disk_last - 33);
assert_eq!(primary.disk_guid, backup.disk_guid);
assert_eq!(primary.disk_guid, gpt.disk_guid);
assert_eq!(primary.type_guid, GPT_ESP_TYPE_GUID, "EFI System partition type");
assert_eq!(primary.part_guid, gpt.partition_guid);
assert_eq!(primary.part_name, "EFI System");
assert_eq!((primary.part_first, primary.part_last), (gpt.first_lba, gpt.last_lba));
assert_eq!((backup.part_first, backup.part_last), (gpt.first_lba, gpt.last_lba));
// v4 / RFC-4122 bits, so any GUID-aware tool accepts them.
assert_eq!(primary.disk_guid[7] & 0xF0, 0x40);
assert_eq!(primary.disk_guid[8] & 0xC0, 0x80);
// ── the partition's byte range IS the ESP image ──
assert_eq!(gpt.first_lba, efi.lba as u64 * 4, "ESP partition starts where the ISO extent starts");
assert_eq!(gpt.len_bytes(), esp_len, "the partition is exactly as long as the FAT image");
let span = &img[gpt.offset_bytes() as usize..][..esp_len as usize];
assert!(span == &esp[..], "the GPT partition's bytes are the staged ESP, byte for byte");
// …and a FAT reader mounted on that span reads the marker file out of it.
let mut cur = std::io::Cursor::new(span.to_vec());
let fs = fatfs::FileSystem::new(&mut cur, fatfs::FsOptions::new()).unwrap();
let mut got = Vec::new();
fs.root_dir().open_file("MARKER.BIN").unwrap().read_to_end(&mut got).unwrap();
assert_eq!(got, marker);
// ── the El-Torito entry: clamped, and the layout SAYS clamped ──
assert!(efi.clamped, "a 34 MiB ESP needs more than u16::MAX sectors");
assert_eq!(efi.catalog_sectors, u16::MAX);
assert_eq!(efi.len, esp_len);
let cat = read_u32_le(&img, 17 * SECTOR + 71) as usize * SECTOR;
assert_eq!(img[cat + 65], PLATFORM_EFI);
assert_eq!(u16::from_le_bytes([img[cat + 102], img[cat + 103]]), u16::MAX, "catalog count clamped");
assert_eq!(read_u32_le(&img, cat + 104), efi.lba, "catalog RBA is the ESP extent");
let line = layout.describe();
assert!(line.contains("GPT HYBRID") && line.contains("CLAMPED"), "receipt line: {line}");
// ── deterministic: the same tree masters byte-identically, GUIDs included ──
let mut again = IsoWriter::new("KORP_APPLIANCE");
again.add_file("efiboot.img", dir.path().join("efiboot.img")).unwrap();
again.add_file("payload.bin", dir.path().join("payload.bin")).unwrap();
again.set_efi_boot("efiboot.img");
again.set_gpt_hybrid(GptHybrid::WhenEspExceedsElTorito);
assert!(again.build_image().unwrap() == img, "hybrid mastering is deterministic");
}
/// **A medium under the ceiling does not change shape.** With
/// `WhenEspExceedsElTorito` — the policy the masterer applies to every UEFI
/// ISO — a small ESP masters BYTE-IDENTICALLY to `Off`: same El-Torito fields,
/// no MBR, no GPT, no tail. gunnar's and holger's media are the ones this
/// protects. `Always` is the control: the same tree DOES gain the GPT, so
/// the identity above is the policy at work and not a hybrid that never lays.
#[test]
fn a_medium_under_the_ceiling_is_byte_identical_with_the_hybrid_policy() {
let dir = tempfile::tempdir().unwrap();
let esp = fat_esp(4 * 1024 * 1024, b"small");
std::fs::write(dir.path().join("efiboot.img"), &esp).unwrap();
let build = |mode: GptHybrid| {
let mut w = IsoWriter::new("GUNNAR_INSTALLER");
w.add_file("efiboot.img", dir.path().join("efiboot.img")).unwrap();
w.set_efi_boot("efiboot.img");
w.set_gpt_hybrid(mode);
let img = w.build_image().unwrap();
(img, *w.layout().unwrap())
};
let (off, off_layout) = build(GptHybrid::Off);
let (when, when_layout) = build(GptHybrid::WhenEspExceedsElTorito);
assert!(off == when, "under the ceiling the hybrid policy must not change a single byte");
assert_eq!(off_layout, when_layout);
assert!(when_layout.gpt.is_none(), "no GPT was laid");
let efi = when_layout.efi.unwrap();
assert!(!efi.clamped);
assert_eq!(efi.catalog_sectors, 8192, "4 MiB = 8192 virtual sectors, exact");
assert_ne!(&off[0..8], b"EFI PART");
assert!(&off[510..512] != &[0x55, 0xAA], "no MBR signature on a plain medium");
assert_eq!(off.len(), when.len(), "no tail appended");
// Control: `Always` on the same tree lays the GPT and grows by the tail.
let (always, always_layout) = build(GptHybrid::Always);
assert!(always != off);
assert_eq!(always.len(), off.len() + GPT_TAIL_ISO_SECTORS as usize * SECTOR);
let gpt = always_layout.gpt.expect("Always lays a GPT even under the ceiling");
let parsed = parse_gpt_at(&always, 1);
assert_eq!((parsed.part_first, parsed.part_last), (gpt.first_lba, gpt.last_lba));
assert_eq!(gpt.len_bytes(), 4 * 1024 * 1024);
let efi = always_layout.efi.unwrap();
assert!(!efi.clamped, "under the ceiling the catalog count stays exact even in a hybrid");
assert_eq!(efi.catalog_sectors, 8192);
// …and the El-Torito fields are the ones the plain medium has.
let cat = read_u32_le(&always, 17 * SECTOR + 71) as usize * SECTOR;
let cat_off = read_u32_le(&off, 17 * SECTOR + 71) as usize * SECTOR;
assert_eq!(&always[cat..cat + 128], &off[cat_off..cat_off + 128], "identical catalog");
}
/// The tail arithmetic and the EDK2 reach constant, spelled out so a change
/// to either is a test failure and not a surprise on a serial console.
#[test]
fn hybrid_constants_are_the_documented_numbers() {
assert_eq!(GPT_TAIL_ISO_SECTORS, 9, "33 LBAs of backup GPT → 9 ISO sectors");
assert_eq!(GPT_FIRST_USABLE_LBA, 34);
assert_eq!(EDK2_OPTICAL_ESP_REACH, 134_215_680, "65 535 × 2048");
assert_eq!(el_torito_sectors_clamped(MAX_BOOT_IMAGE_BYTES), (u16::MAX, false));
assert_eq!(el_torito_sectors_clamped(MAX_BOOT_IMAGE_BYTES + 1), (u16::MAX, true));
assert_eq!(el_torito_sectors_clamped(0), (1, false));
// korp's measured ESP (oden 2026-08-22): 88 385 024 B — clamped, and
// within EDK2's optical reach.
assert_eq!(el_torito_sectors_clamped(88_385_024), (u16::MAX, true));
assert!(88_385_024 < EDK2_OPTICAL_ESP_REACH);
}
/// A boot image that was never staged is a hard error, not a silent zero LBA.
#[test]
fn unstaged_boot_image_errors() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.bin"), b"x").unwrap();
let mut w = IsoWriter::new("ERR");
w.add_file("a.bin", dir.path().join("a.bin")).unwrap();
w.set_efi_boot("efiboot.img"); // never added
assert!(w.build_image().is_err(), "unstaged EFI boot image is rejected");
}
}