xtask/src/image_formats.rs
//! The three on-disk formats the appliance ISO is made of, written by hand.
//!
//! `cpio` (newc) for the initramfs, `FAT16` for the EFI System Partition, and
//! `ISO-9660` + El Torito for the ISO itself. Nothing here spawns `cpio`,
//! `mkfs.vfat`, `mtools`, `xorriso`, `genisoimage` or `grub-mkrescue`.
//!
//! # Why by hand
//!
//! Two reasons, and only the second is about dependencies.
//!
//! **`xtask` has none, deliberately** (`xtask/Cargo.toml`): a workspace member
//! takes part in resolver-2 feature unification, so a crate added here could
//! turn a feature on in a crate the appliance links, and the dependency guard's
//! job is not to be the thing it guards against.
//!
//! **This IS a twin, all three formats, and here is the whole pointer.** The
//! house rule says reuse; this file does not, so it owes an exact account of what it is a
//! copy of and what stops it being an edge. Measured 2026-08-07 — every one of
//! the three has a pure-Rust equivalent in `../edda`:
//!
//! | this file | the fleet's own | reachable from gunnar? |
//! |----------------------|----------------------------------|------------------------|
//! | [`cpio_newc`] | `korp_installer::initramfs` | NO — `publish = false` |
//! | [`fat16`] | `korp_installer::esp::build_esp_image` | NO — same crate |
//! | [`iso9660_eltorito`] | `nornir_build_thing::IsoWriter` | by REGISTRY, yes |
//!
//! The first two have no registry route at all: `korp-installer` is
//! `publish = false`, so a path dependency into `../edda` is the ONLY way to reach
//! them. The appliance design argues against sibling path deps on the
//! principle that "Gunnar is meant to build standalone from a fresh clone" —
//! stated there about Skidbladnir specifically, and already suspended once by the
//! `[patch.crates-io]` pointing `nornir-git` at `../edda`, so treat it as the
//! standing preference it is rather than an absolute. The third IS a published
//! crate gunnar already uses from the registry (`gunnar-cli` deps
//! `nornir-build-thing`), so only the ISO-9660 leg is a candidate on that axis.
//!
//! What is absolute is the rule above, and it is the one that decides this file:
//! `xtask` is a WORKSPACE MEMBER, so anything it links unifies features with the
//! crates the appliance links. That is precisely why `installtest` — which is NOT
//! a member (see `Cargo.toml`'s `members`) — is free to path-dep `tunnr-vm` and
//! boot through `tunnr_vm::boot_test`, while nothing in `xtask` may.
//!
//! What would actually retire this file is therefore a design change, not a
//! refactor: move the ISO leg behind `crates/gunnar-build-thing` (which already
//! deps `nornir-build-thing` from the registry) and have `xtask` INVOKE it as a
//! tool rather than link it — which is the same "invoked as a tool, never as a
//! dependency" rule that put `xtask` at zero deps to begin with. See the twin
//! block in `.nornir/build-variants.toml` under the `gunnar-appliance-iso` cell.
//!
//! Reuse is honoured where it can be paid for in an *artefact* rather than an
//! edge: the first-stage UEFI loader is vendored as
//! `assets/boot/korp-boot-bootx64.efi` (see its README), because that one is six
//! hundred lines of `unsafe` and is already proven booting.
//!
//! **The tools would have to be preflighted anyway.** `mkfs.vfat` is in
//! `dosfstools`, `xorriso` in its own package, and neither is installed on
//! the desktop. An ISO build that fails on a fresh machine with "command not found" is
//! not a build; and `cargo xtask preflight` exists precisely so a missing tool
//! is a named failure rather than a skipped test.
//!
//! # What is asserted, and where
//!
//! Every writer here is a pure `Vec<u8>` producer, so the tests in
//! `xtask/tests/image_formats.rs` read the bytes back out: a cpio header
//! parsed field by field, a FAT directory entry located through the BPB, an
//! El Torito catalog checksum recomputed. The ISO is then booted, which is the
//! only assertion that covers all three at once.
use std::fmt::Write as _;
// ════════════════════════════════════════════════════════════════════════════
// cpio, `newc` (SVR4 without CRC): the initramfs
// ════════════════════════════════════════════════════════════════════════════
/// One member of the archive.
#[derive(Debug, Clone)]
pub struct CpioEntry {
/// The path **without** a leading slash: `init`, `data`, `dev/console`.
/// A leading slash makes the kernel's unpacker create a directory called
/// `""` and is the single most common way to produce an initramfs that
/// unpacks to nothing.
pub name: String,
/// `st_mode`: the file type bits **and** the permission bits.
pub mode: u32,
/// The file's contents. Empty for directories and device nodes.
pub data: Vec<u8>,
/// `(major, minor)` for a character device; `None` otherwise.
pub rdev: Option<(u32, u32)>,
}
/// `S_IFREG`
pub const MODE_FILE: u32 = 0o100_000;
/// `S_IFDIR`
pub const MODE_DIR: u32 = 0o040_000;
/// `S_IFCHR`
pub const MODE_CHAR: u32 = 0o020_000;
impl CpioEntry {
pub fn file(name: &str, mode: u32, data: Vec<u8>) -> CpioEntry {
CpioEntry {
name: name.to_string(),
mode: MODE_FILE | mode,
data,
rdev: None,
}
}
pub fn dir(name: &str, mode: u32) -> CpioEntry {
CpioEntry {
name: name.to_string(),
mode: MODE_DIR | mode,
data: Vec::new(),
rdev: None,
}
}
/// A character device node: `/dev/console` and `/dev/null`.
///
/// `/dev/console` is not optional and its absence is silent. The kernel
/// opens it to give `init` its stdin, stdout and stderr
/// (`init/main.c`, `console_on_rootfs`); if it is missing, init starts with
/// **file descriptors 0, 1 and 2 closed**, the boot log says only
/// "Warning: unable to open an initial console", and the first socket the
/// server opens becomes fd 0. Every `tracing` line then goes into that
/// socket.
pub fn chardev(name: &str, mode: u32, major: u32, minor: u32) -> CpioEntry {
CpioEntry {
name: name.to_string(),
mode: MODE_CHAR | mode,
data: Vec::new(),
rdev: Some((major, minor)),
}
}
}
/// Encode a `newc` archive, ending with the `TRAILER!!!` record.
///
/// Deterministic: `mtime` 0, `uid`/`gid` 0, inode numbers assigned in order.
/// Two runs over the same inputs produce byte-identical output, which is what
/// makes the ISO's own hash meaningful.
pub fn cpio_newc(entries: &[CpioEntry]) -> Vec<u8> {
let mut out = Vec::new();
for (i, e) in entries.iter().enumerate() {
let (major, minor) = e.rdev.unwrap_or((0, 0));
write_cpio_header(
&mut out,
i as u32 + 1,
e.mode,
e.data.len() as u32,
major,
minor,
&e.name,
);
out.extend_from_slice(&e.data);
pad4(&mut out);
}
// The trailer's `nlink` is 1 and its name is exactly `TRAILER!!!`. A
// trailer with nlink 0 is skipped by some unpackers and the archive then
// appears truncated.
write_cpio_header(&mut out, 0, 0, 0, 0, 0, "TRAILER!!!");
out
}
#[allow(clippy::too_many_arguments)]
fn write_cpio_header(
out: &mut Vec<u8>,
ino: u32,
mode: u32,
filesize: u32,
rdevmajor: u32,
rdevminor: u32,
name: &str,
) {
let namesize = name.len() as u32 + 1; // the NUL counts
let mut header = String::with_capacity(110);
header.push_str("070701");
for field in [
ino, mode, 0, // uid
0, // gid
1, // nlink
0, // mtime
filesize, 0, // devmajor
0, // devminor
rdevmajor, rdevminor, namesize, 0, // check, always zero in newc
] {
let _ = write!(header, "{field:08X}");
}
debug_assert_eq!(header.len(), 110, "a newc header is exactly 110 bytes");
out.extend_from_slice(header.as_bytes());
out.extend_from_slice(name.as_bytes());
out.push(0);
// The header + name is padded to four; the DATA is padded separately by the
// caller. Conflating the two produces an archive whose second member is
// misaligned and whose first member looks perfect.
pad4(out);
}
fn pad4(out: &mut Vec<u8>) {
while out.len() % 4 != 0 {
out.push(0);
}
}
// ════════════════════════════════════════════════════════════════════════════
// FAT16: the EFI System Partition
// ════════════════════════════════════════════════════════════════════════════
/// A file to place in the FAT volume, by its full path within it.
#[derive(Debug, Clone)]
pub struct FatFile {
/// `EFI/BOOT/BOOTX64.EFI`, `vmlinuz`, `korp/cmdline`. Every component must
/// fit 8.3. This writer emits no long-file-name entries, because every
/// name the appliance needs already fits and an LFN checksum is a second
/// way to be wrong.
pub path: String,
pub bytes: Vec<u8>,
}
const SECTOR: usize = 512;
const ROOT_ENTRIES: usize = 512;
const RESERVED_SECTORS: usize = 1;
const NUM_FATS: usize = 2;
/// Format a FAT16 volume holding `files`, sized to fit them.
///
/// # Why FAT16 and not FAT32
///
/// The volume is tens of megabytes. FAT32's minimum is 65 525 clusters, which
/// at the 512-byte sector this uses means a 32 MiB floor before the first file
/// and FAT16's ceiling is 65 524 clusters, so at 32 KiB clusters it reaches
/// 2 GiB. FAT16 covers the entire range this can ever need with a fixed-size
/// root directory and no `FSInfo` sector to keep consistent.
///
/// # The cluster-count trap
///
/// FAT type is **not** declared anywhere the driver trusts; it is *derived*
/// from the cluster count (`< 4085` is FAT12, `< 65525` is FAT16). The
/// `"FAT16 "` string at offset 0x36 is documentation, not a declaration. So a
/// small volume with a large cluster size silently becomes FAT12 and every
/// entry in the FAT is then read as twelve bits. [`fat16`] therefore searches
/// for a cluster size that lands the count strictly inside FAT16's range and
/// fails loudly if none does.
pub fn fat16(files: &[FatFile], volume_label: &str) -> Result<Vec<u8>, String> {
// Directories are one cluster each; count them so the sizing is not an
// underestimate that only fails on a full volume.
let mut dirs: Vec<String> = Vec::new();
for f in files {
let mut prefix = String::new();
let parts: Vec<&str> = f.path.split('/').collect();
for component in &parts[..parts.len().saturating_sub(1)] {
if !prefix.is_empty() {
prefix.push('/');
}
prefix.push_str(component);
if !dirs.contains(&prefix) {
dirs.push(prefix.clone());
}
}
}
let payload: usize = files.iter().map(|f| f.bytes.len()).sum();
// Try each legal cluster size, smallest first: less slack, and a cluster
// count that stays comfortably inside FAT16's range.
for spc in [4usize, 8, 16, 32, 64] {
let cluster = spc * SECTOR;
let mut data_clusters = dirs.len(); // one cluster per directory
for f in files {
data_clusters += f.bytes.len().div_ceil(cluster).max(1);
}
// Slack: a spare cluster per file for the rounding above, plus a floor
// so a nearly-empty volume is still a legal FAT16.
let data_clusters = (data_clusters + files.len() + 16).max(4200);
if data_clusters > 65_524 {
continue;
}
if data_clusters < 4085 {
continue;
}
let fat_bytes = (data_clusters + 2) * 2;
let sectors_per_fat = fat_bytes.div_ceil(SECTOR);
let root_sectors = (ROOT_ENTRIES * 32) / SECTOR;
let total_sectors =
RESERVED_SECTORS + NUM_FATS * sectors_per_fat + root_sectors + data_clusters * spc;
let _ = payload;
return build_fat16(
files,
&dirs,
volume_label,
spc,
sectors_per_fat,
root_sectors,
data_clusters,
total_sectors,
);
}
Err(format!(
"no FAT16 cluster size fits {} files totalling {} bytes: the volume is either under \
4085 clusters (which is FAT12 however the boot sector is labelled) or over 65524 \
(which is FAT32)",
files.len(),
payload
))
}
#[allow(clippy::too_many_arguments)]
fn build_fat16(
files: &[FatFile],
dirs: &[String],
volume_label: &str,
spc: usize,
sectors_per_fat: usize,
root_sectors: usize,
data_clusters: usize,
total_sectors: usize,
) -> Result<Vec<u8>, String> {
let cluster_bytes = spc * SECTOR;
let fat_start = RESERVED_SECTORS;
let root_start = fat_start + NUM_FATS * sectors_per_fat;
let data_start = root_start + root_sectors;
let mut image = vec![0u8; total_sectors * SECTOR];
// ── boot sector / BPB ───────────────────────────────────────────────────
{
let bs = &mut image[..SECTOR];
// A jump the firmware never executes, but a FAT driver that sanity
// checks the first byte rejects a volume that does not start with
// 0xEB or 0xE9, and EDK2 is one of them.
bs[0..3].copy_from_slice(&[0xEB, 0x3C, 0x90]);
bs[3..11].copy_from_slice(b"MSWIN4.1");
bs[11..13].copy_from_slice(&(SECTOR as u16).to_le_bytes());
bs[13] = spc as u8;
bs[14..16].copy_from_slice(&(RESERVED_SECTORS as u16).to_le_bytes());
bs[16] = NUM_FATS as u8;
bs[17..19].copy_from_slice(&(ROOT_ENTRIES as u16).to_le_bytes());
// total_sectors_16 stays 0 whenever the count does not fit, and then
// total_sectors_32 carries it. Setting both is legal; setting the
// 16-bit one to a truncated value is the failure this avoids.
if total_sectors <= u16::MAX as usize {
bs[19..21].copy_from_slice(&(total_sectors as u16).to_le_bytes());
}
bs[21] = 0xF8; // fixed disk
bs[22..24].copy_from_slice(&(sectors_per_fat as u16).to_le_bytes());
bs[24..26].copy_from_slice(&32u16.to_le_bytes()); // sectors per track
bs[26..28].copy_from_slice(&8u16.to_le_bytes()); // heads
bs[28..32].copy_from_slice(&0u32.to_le_bytes()); // hidden sectors
bs[32..36].copy_from_slice(&(total_sectors as u32).to_le_bytes());
bs[36] = 0x80; // drive number
bs[38] = 0x29; // extended boot signature: the three fields below are valid
bs[39..43].copy_from_slice(&0x4755_4E4Au32.to_le_bytes()); // "GUNJ"
let mut label = [b' '; 11];
for (i, b) in volume_label.bytes().take(11).enumerate() {
label[i] = b.to_ascii_uppercase();
}
bs[43..54].copy_from_slice(&label);
bs[54..62].copy_from_slice(b"FAT16 ");
bs[510] = 0x55;
bs[511] = 0xAA;
}
// ── allocate clusters ───────────────────────────────────────────────────
//
// A directory gets one cluster; a file gets as many as it needs. `next` is
// the next free cluster number, starting at 2 because 0 and 1 are the two
// reserved FAT entries and are not addressable.
let mut fat: Vec<u16> = vec![0; data_clusters + 2];
fat[0] = 0xFFF8;
fat[1] = 0xFFFF;
let mut next: u16 = 2;
let mut alloc = |count: usize, fat: &mut Vec<u16>| -> Result<u16, String> {
let first = next;
for i in 0..count {
let this = next;
if (this as usize) >= fat.len() {
return Err("the FAT ran out of clusters; the sizing above is wrong".into());
}
next += 1;
fat[this as usize] = if i + 1 == count { 0xFFFF } else { next };
}
Ok(first)
};
// Directories, parents before children, so a child's entry can be written
// into a parent whose cluster is already known.
let mut dir_cluster: Vec<(String, u16)> = Vec::new();
let mut sorted_dirs: Vec<&String> = dirs.iter().collect();
sorted_dirs.sort_by_key(|d| d.matches('/').count());
for d in sorted_dirs {
let c = alloc(1, &mut fat)?;
dir_cluster.push((d.clone(), c));
}
// Files.
let mut file_cluster: Vec<u16> = Vec::new();
for f in files {
let need = f.bytes.len().div_ceil(cluster_bytes).max(1);
file_cluster.push(alloc(need, &mut fat)?);
}
// ── directory contents ──────────────────────────────────────────────────
//
// Built in memory first: a subdirectory's `.` and `..` entries have to be
// written before any child entry, and appending to a `Vec` keeps that
// ordering obvious.
let mut root_entries: Vec<u8> = Vec::new();
let mut sub_entries: Vec<(u16, Vec<u8>)> = Vec::new();
for (path, cluster) in &dir_cluster {
let (parent, name) = match path.rsplit_once('/') {
Some((p, n)) => (Some(p.to_string()), n.to_string()),
None => (None, path.clone()),
};
let mut body = Vec::new();
body.extend_from_slice(&dir_entry(".", *cluster, 0, true));
let parent_cluster = match &parent {
None => 0, // `..` of a first-level directory points at the root, and
// the root's cluster number is written as 0
Some(p) => dir_cluster
.iter()
.find(|(d, _)| d == p)
.map(|(_, c)| *c)
.ok_or_else(|| format!("parent directory {p:?} was never allocated"))?,
};
body.extend_from_slice(&dir_entry("..", parent_cluster, 0, true));
sub_entries.push((*cluster, body));
let entry = dir_entry(&name, *cluster, 0, true);
match &parent {
None => root_entries.extend_from_slice(&entry),
Some(p) => {
let pc = dir_cluster
.iter()
.find(|(d, _)| d == p)
.map(|(_, c)| *c)
.expect("checked above");
push_into(&mut sub_entries, pc, &entry);
}
}
}
for (i, f) in files.iter().enumerate() {
let (parent, name) = match f.path.rsplit_once('/') {
Some((p, n)) => (Some(p.to_string()), n.to_string()),
None => (None, f.path.clone()),
};
let entry = dir_entry(&name, file_cluster[i], f.bytes.len() as u32, false);
match &parent {
None => root_entries.extend_from_slice(&entry),
Some(p) => {
let pc = dir_cluster
.iter()
.find(|(d, _)| d == p)
.map(|(_, c)| *c)
.ok_or_else(|| format!("parent directory {p:?} was never allocated"))?;
push_into(&mut sub_entries, pc, &entry);
}
}
}
if root_entries.len() > ROOT_ENTRIES * 32 {
return Err(format!(
"{} root-directory entries do not fit in the fixed {ROOT_ENTRIES}-entry root of a \
FAT16 volume",
root_entries.len() / 32
));
}
// ── write it all out ────────────────────────────────────────────────────
for f in 0..NUM_FATS {
let off = (fat_start + f * sectors_per_fat) * SECTOR;
for (i, entry) in fat.iter().enumerate() {
let at = off + i * 2;
if at + 2 <= image.len() {
image[at..at + 2].copy_from_slice(&entry.to_le_bytes());
}
}
}
image[root_start * SECTOR..root_start * SECTOR + root_entries.len()]
.copy_from_slice(&root_entries);
let cluster_offset = |c: u16| -> usize { (data_start + (c as usize - 2) * spc) * SECTOR };
for (cluster, body) in &sub_entries {
if body.len() > cluster_bytes {
return Err(format!(
"a subdirectory needs {} bytes and this writer gives every directory one \
{cluster_bytes}-byte cluster",
body.len()
));
}
let at = cluster_offset(*cluster);
image[at..at + body.len()].copy_from_slice(body);
}
for (i, f) in files.iter().enumerate() {
let at = cluster_offset(file_cluster[i]);
image[at..at + f.bytes.len()].copy_from_slice(&f.bytes);
}
Ok(image)
}
fn push_into(dirs: &mut [(u16, Vec<u8>)], cluster: u16, entry: &[u8]) {
if let Some((_, body)) = dirs.iter_mut().find(|(c, _)| *c == cluster) {
body.extend_from_slice(entry);
}
}
/// One 32-byte 8.3 directory entry.
fn dir_entry(name: &str, cluster: u16, size: u32, is_dir: bool) -> [u8; 32] {
let mut e = [0u8; 32];
let mut field = [b' '; 11];
if name == "." {
field[0] = b'.';
} else if name == ".." {
field[0] = b'.';
field[1] = b'.';
} else {
let (base, ext) = match name.rsplit_once('.') {
Some((b, x)) => (b, x),
None => (name, ""),
};
for (i, b) in base.bytes().take(8).enumerate() {
field[i] = b.to_ascii_uppercase();
}
for (i, b) in ext.bytes().take(3).enumerate() {
field[8 + i] = b.to_ascii_uppercase();
}
}
e[0..11].copy_from_slice(&field);
e[11] = if is_dir { 0x10 } else { 0x20 };
// 1980-01-01 00:00:00, the FAT epoch. A zero date is legal and some drivers
// display it as garbage; the epoch is the smallest honest value.
e[22..24].copy_from_slice(&0u16.to_le_bytes());
e[24..26].copy_from_slice(&0x0021u16.to_le_bytes());
e[26..28].copy_from_slice(&cluster.to_le_bytes());
e[28..32].copy_from_slice(&size.to_le_bytes());
e
}
// ════════════════════════════════════════════════════════════════════════════
// ISO-9660 + El Torito
// ════════════════════════════════════════════════════════════════════════════
/// 2 048 bytes, the ISO-9660 logical block, not the 512-byte disk sector the
/// El Torito catalog counts in. Conflating the two is how a boot image ends up
/// four times too small.
pub const ISO_BLOCK: usize = 2048;
/// The largest EFI boot image an El Torito catalog can describe.
///
/// `sector_count` in a boot catalog entry is a **u16 of 512-byte sectors**, and
/// EDK2's `PartitionDxe` uses exactly that to size the block device it exposes
/// for the boot image (`ElTorito.c`: `Size = Catalog->Boot.SectorCount`,
/// `SubBlockSize = 512`). A larger image is not truncated at boot with an
/// error; the firmware exposes a partition that stops in the middle of the FAT,
/// and the failure surfaces as "no bootable device".
pub const ELTORITO_MAX_BOOT_IMAGE: usize = 65_535 * 512;
/// An ISO-9660 image with a single El Torito EFI boot entry.
///
/// One file in the root directory, the ESP itself, so the ISO is
/// self-describing: `mount -o loop` shows `EFI.IMG`, and the boot catalog
/// points at the same extent rather than at a hidden one.
pub fn iso9660_eltorito(
volume_id: &str,
boot_image_name: &str,
boot_image: &[u8],
) -> Result<Vec<u8>, String> {
if boot_image.len() > ELTORITO_MAX_BOOT_IMAGE {
return Err(format!(
"the EFI boot image is {} bytes and El Torito's sector_count is a u16 of 512-byte \
sectors, so {ELTORITO_MAX_BOOT_IMAGE} is the ceiling. Shrink the initramfs \
(compress it) or move to a GPT-partitioned hybrid image.",
boot_image.len()
));
}
// ── the layout, in 2 048-byte blocks ────────────────────────────────────
const PVD_LBA: u32 = 16;
const BOOT_RECORD_LBA: u32 = 17;
const TERMINATOR_LBA: u32 = 18;
const PATH_TABLE_L_LBA: u32 = 19;
const PATH_TABLE_M_LBA: u32 = 20;
const ROOT_DIR_LBA: u32 = 21;
const CATALOG_LBA: u32 = 22;
const BOOT_IMAGE_LBA: u32 = 23;
let boot_blocks = boot_image.len().div_ceil(ISO_BLOCK) as u32;
let total_blocks = BOOT_IMAGE_LBA + boot_blocks;
let mut iso = vec![0u8; total_blocks as usize * ISO_BLOCK];
let block = |iso: &mut Vec<u8>, lba: u32| -> std::ops::Range<usize> {
let start = lba as usize * ISO_BLOCK;
let _ = iso;
start..start + ISO_BLOCK
};
// ── root directory ──────────────────────────────────────────────────────
let iso_name = format!("{};1", boot_image_name.to_ascii_uppercase());
let mut root = Vec::new();
root.extend_from_slice(&iso_dir_record(ROOT_DIR_LBA, ISO_BLOCK as u32, true, &[0]));
root.extend_from_slice(&iso_dir_record(ROOT_DIR_LBA, ISO_BLOCK as u32, true, &[1]));
root.extend_from_slice(&iso_dir_record(
BOOT_IMAGE_LBA,
boot_image.len() as u32,
false,
iso_name.as_bytes(),
));
let root_len = root.len();
let r = block(&mut iso, ROOT_DIR_LBA);
iso[r.start..r.start + root_len].copy_from_slice(&root);
// ── primary volume descriptor ───────────────────────────────────────────
let mut pvd = vec![0u8; ISO_BLOCK];
pvd[0] = 1;
pvd[1..6].copy_from_slice(b"CD001");
pvd[6] = 1;
fill_achars(&mut pvd[8..40], "LINUX");
fill_dchars(&mut pvd[40..72], volume_id);
both_u32(&mut pvd[80..88], total_blocks);
both_u16(&mut pvd[120..124], 1); // volume set size
both_u16(&mut pvd[124..128], 1); // volume sequence number
both_u16(&mut pvd[128..132], ISO_BLOCK as u16);
// The path table exists because the standard requires it. Every EFI
// firmware this targets walks directory records instead, but a missing
// path table is the kind of omission a stricter driver rejects and a
// permissive one ignores, and "works on the firmware I tested" is not a
// property, it is a coincidence.
let path_table = iso_path_table(ROOT_DIR_LBA, true);
let path_table_m = iso_path_table(ROOT_DIR_LBA, false);
both_u32(&mut pvd[132..140], path_table.len() as u32);
pvd[140..144].copy_from_slice(&PATH_TABLE_L_LBA.to_le_bytes());
pvd[148..152].copy_from_slice(&PATH_TABLE_M_LBA.to_be_bytes());
let root_record = iso_dir_record(ROOT_DIR_LBA, ISO_BLOCK as u32, true, &[0]);
pvd[156..156 + 34].copy_from_slice(&root_record);
fill_dchars(&mut pvd[190..318], "GUNNAR");
fill_achars(&mut pvd[318..446], "GUNNAR");
fill_achars(&mut pvd[446..574], "CARGO XTASK ISO");
fill_achars(&mut pvd[574..702], "GUNNAR APPLIANCE");
for range in [702..739, 739..776, 776..813] {
fill_dchars(&mut pvd[range], "");
}
for at in [813usize, 830, 847, 864] {
pvd[at..at + 17].copy_from_slice(b"0000000000000000\0");
}
pvd[881] = 1; // file structure version
let p = block(&mut iso, PVD_LBA);
iso[p.clone()].copy_from_slice(&pvd);
// ── El Torito boot record volume descriptor ─────────────────────────────
let mut br = vec![0u8; ISO_BLOCK];
br[0] = 0;
br[1..6].copy_from_slice(b"CD001");
br[6] = 1;
br[7..7 + 23].copy_from_slice(b"EL TORITO SPECIFICATION");
br[71..75].copy_from_slice(&CATALOG_LBA.to_le_bytes());
let b = block(&mut iso, BOOT_RECORD_LBA);
iso[b.clone()].copy_from_slice(&br);
// ── terminator ──────────────────────────────────────────────────────────
let mut term = vec![0u8; ISO_BLOCK];
term[0] = 255;
term[1..6].copy_from_slice(b"CD001");
term[6] = 1;
let t = block(&mut iso, TERMINATOR_LBA);
iso[t.clone()].copy_from_slice(&term);
// ── path tables ─────────────────────────────────────────────────────────
let l = block(&mut iso, PATH_TABLE_L_LBA);
iso[l.start..l.start + path_table.len()].copy_from_slice(&path_table);
let m = block(&mut iso, PATH_TABLE_M_LBA);
iso[m.start..m.start + path_table_m.len()].copy_from_slice(&path_table_m);
// ── boot catalog ────────────────────────────────────────────────────────
let catalog = eltorito_catalog(BOOT_IMAGE_LBA, boot_image.len());
let c = block(&mut iso, CATALOG_LBA);
iso[c.start..c.start + catalog.len()].copy_from_slice(&catalog);
// ── the boot image itself ───────────────────────────────────────────────
let at = BOOT_IMAGE_LBA as usize * ISO_BLOCK;
iso[at..at + boot_image.len()].copy_from_slice(boot_image);
Ok(iso)
}
/// The 64-byte El Torito boot catalog: a validation entry and one EFI entry.
///
/// Exposed so a test can recompute the checksum rather than trusting the
/// producer to have computed it. The validation entry is the one structure
/// here whose corruption makes the firmware ignore the catalog **silently**.
pub fn eltorito_catalog(boot_lba: u32, boot_len: usize) -> Vec<u8> {
let mut cat = vec![0u8; 64];
// Validation entry.
cat[0] = 0x01; // header id
cat[1] = 0xEF; // platform: EFI
cat[30] = 0x55;
cat[31] = 0xAA;
// The checksum is chosen so the sixteen little-endian u16 words of the
// validation entry sum to zero mod 2^16.
let mut sum: u16 = 0;
for i in (0..32).step_by(2) {
sum = sum.wrapping_add(u16::from_le_bytes([cat[i], cat[i + 1]]));
}
let checksum = (0u16).wrapping_sub(sum);
cat[28..30].copy_from_slice(&checksum.to_le_bytes());
// The default entry: no emulation, so the firmware exposes the image as a
// block device rather than pretending it is a floppy.
cat[32] = 0x88; // bootable
cat[33] = 0x00; // no emulation
cat[34..36].copy_from_slice(&0u16.to_le_bytes()); // load segment: firmware default
cat[36] = 0; // system type
let sectors = boot_len.div_ceil(512) as u16;
cat[38..40].copy_from_slice(§ors.to_le_bytes());
cat[40..44].copy_from_slice(&boot_lba.to_le_bytes());
cat
}
/// A 34-byte-or-longer directory record.
fn iso_dir_record(extent: u32, size: u32, is_dir: bool, name: &[u8]) -> Vec<u8> {
let mut rec = vec![0u8; 33 + name.len()];
rec[1] = 0; // extended attribute length
both_u32(&mut rec[2..10], extent);
both_u32(&mut rec[10..18], size);
// Recording date and time, seven bytes: years since 1900, month, day, hour,
// minute, second, GMT offset in 15-minute intervals.
rec[18..25].copy_from_slice(&[80, 1, 1, 0, 0, 0, 0]);
rec[25] = if is_dir { 0x02 } else { 0x00 };
both_u16(&mut rec[28..32], 1); // volume sequence number
rec[32] = name.len() as u8;
rec[33..].copy_from_slice(name);
if rec.len() % 2 != 0 {
rec.push(0);
}
rec[0] = rec.len() as u8;
rec
}
fn iso_path_table(root_lba: u32, little: bool) -> Vec<u8> {
let mut t = vec![0u8; 10];
t[0] = 1; // directory identifier length
t[1] = 0; // extended attribute length
if little {
t[2..6].copy_from_slice(&root_lba.to_le_bytes());
t[6..8].copy_from_slice(&1u16.to_le_bytes());
} else {
t[2..6].copy_from_slice(&root_lba.to_be_bytes());
t[6..8].copy_from_slice(&1u16.to_be_bytes());
}
t[8] = 0; // the root's identifier is a single NUL
t
}
/// ISO-9660's "both-endian" u32: the value little-endian, then big-endian.
///
/// Not a redundancy anybody may skip. A reader is entitled to consult either
/// half, and the two disagreeing is undefined rather than tolerated.
fn both_u32(out: &mut [u8], v: u32) {
out[0..4].copy_from_slice(&v.to_le_bytes());
out[4..8].copy_from_slice(&v.to_be_bytes());
}
fn both_u16(out: &mut [u8], v: u16) {
out[0..2].copy_from_slice(&v.to_le_bytes());
out[2..4].copy_from_slice(&v.to_be_bytes());
}
fn fill_achars(out: &mut [u8], value: &str) {
for b in out.iter_mut() {
*b = b' ';
}
for (i, b) in value.bytes().take(out.len()).enumerate() {
out[i] = b.to_ascii_uppercase();
}
}
fn fill_dchars(out: &mut [u8], value: &str) {
fill_achars(out, value);
}