Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

xtask/tests/image_formats.rs

//! The three image formats, read back out of the bytes they produce.
//!
//! Every assertion here parses the output rather than checking a length or a
//! return value. A cpio writer that produced 110 bytes of ASCII and got the
//! field order wrong has the right size and the wrong archive; a FAT image with
//! a plausible BPB and a directory entry in the wrong sector mounts as empty.
//!
//! The ISO is booted by `cargo xtask iso --prove`, which is the only assertion
//! that covers all three at once — but a boot failure says "no bootable device"
//! and nothing else, so the field-level checks are what make it debuggable.

use xtask::image_formats::{
    cpio_newc, eltorito_catalog, fat16, iso9660_eltorito, CpioEntry, FatFile, MODE_CHAR, MODE_DIR,
    MODE_FILE,
};

// ── cpio ────────────────────────────────────────────────────────────────────

/// Parse one `newc` header at `at`, returning its fields and where the next
/// record starts. Written here rather than in the producer so the test is not
/// reading the producer's own idea of the layout.
fn parse_newc(archive: &[u8], at: usize) -> (String, u32, u32, u32, u32, usize) {
    assert_eq!(
        &archive[at..at + 6],
        b"070701",
        "a newc record must start with the magic 070701"
    );
    let field = |i: usize| -> u32 {
        let s = std::str::from_utf8(&archive[at + 6 + i * 8..at + 6 + i * 8 + 8]).unwrap();
        u32::from_str_radix(s, 16).unwrap()
    };
    let mode = field(1);
    let filesize = field(6);
    let rdevmajor = field(9);
    let rdevminor = field(10);
    let namesize = field(11) as usize;
    let name_at = at + 110;
    let name = String::from_utf8_lossy(&archive[name_at..name_at + namesize - 1]).into_owned();
    let after_name = (name_at + namesize).next_multiple_of(4);
    let next = (after_name + filesize as usize).next_multiple_of(4);
    (name, mode, filesize, rdevmajor, rdevminor, next)
}

#[test]
fn a_cpio_archive_round_trips_every_field_the_kernel_reads() {
    let archive = cpio_newc(&[
        CpioEntry::file("init", 0o755, b"ELF...".to_vec()),
        CpioEntry::dir("dev", 0o755),
        CpioEntry::chardev("dev/console", 0o600, 5, 1),
    ]);

    let (name, mode, size, _, _, next) = parse_newc(&archive, 0);
    assert_eq!(
        name, "init",
        "the first member must not carry a leading slash"
    );
    assert_eq!(
        mode,
        MODE_FILE | 0o755,
        "/init must be a regular file the kernel can execute"
    );
    assert_eq!(size, 6);

    let (name, mode, size, _, _, next) = parse_newc(&archive, next);
    assert_eq!(name, "dev");
    assert_eq!(mode, MODE_DIR | 0o755);
    assert_eq!(size, 0, "a directory record carries no data");

    let (name, mode, size, major, minor, next) = parse_newc(&archive, next);
    assert_eq!(name, "dev/console");
    assert_eq!(mode, MODE_CHAR | 0o600);
    assert_eq!(size, 0);
    // The whole point of the entry. Without 5:1 the kernel cannot open a
    // console for init and fds 0..2 stay closed.
    assert_eq!(
        (major, minor),
        (5, 1),
        "/dev/console is character device 5:1"
    );

    let (name, _, _, _, _, _) = parse_newc(&archive, next);
    assert_eq!(name, "TRAILER!!!", "the archive must end with a trailer");
}

#[test]
fn the_payload_of_a_cpio_member_is_where_the_header_says_it_is() {
    // The failure this catches: padding the header and the data with one rule
    // instead of two. The first member is then perfect and the second is
    // shifted, which reads as a corrupt archive with no clue where.
    let archive = cpio_newc(&[
        CpioEntry::file("a", 0o644, b"first".to_vec()),
        CpioEntry::file("bb", 0o644, b"second".to_vec()),
    ]);
    let (_, _, size, _, _, next) = parse_newc(&archive, 0);
    let data_at = (110 + 2usize).next_multiple_of(4);
    assert_eq!(&archive[data_at..data_at + size as usize], b"first");

    let (name, _, size2, _, _, _) = parse_newc(&archive, next);
    assert_eq!(name, "bb");
    let data2_at = (next + 110 + 3).next_multiple_of(4);
    assert_eq!(&archive[data2_at..data2_at + size2 as usize], b"second");
}

#[test]
fn the_archive_is_deterministic() {
    let build = || {
        cpio_newc(&[
            CpioEntry::file("init", 0o755, vec![7; 100]),
            CpioEntry::dir("data", 0o755),
        ])
    };
    assert_eq!(build(), build(), "two runs must produce the same bytes");
}

// ── FAT16 ───────────────────────────────────────────────────────────────────

struct Bpb {
    bytes_per_sector: usize,
    sectors_per_cluster: usize,
    reserved: usize,
    num_fats: usize,
    root_entries: usize,
    sectors_per_fat: usize,
    total_sectors: usize,
}

fn parse_bpb(img: &[u8]) -> Bpb {
    let u16le = |at: usize| u16::from_le_bytes([img[at], img[at + 1]]) as usize;
    let total16 = u16le(19);
    let total32 = u32::from_le_bytes([img[32], img[33], img[34], img[35]]) as usize;
    Bpb {
        bytes_per_sector: u16le(11),
        sectors_per_cluster: img[13] as usize,
        reserved: u16le(14),
        num_fats: img[16] as usize,
        root_entries: u16le(17),
        sectors_per_fat: u16le(22),
        total_sectors: if total16 != 0 { total16 } else { total32 },
    }
}

/// Find an 8.3 entry in a 32-byte-record directory area, returning
/// `(first_cluster, size, attributes)`.
fn find_entry(dir: &[u8], name: &str) -> Option<(u16, u32, u8)> {
    let mut field = [b' '; 11];
    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();
    }
    for rec in dir.chunks_exact(32) {
        if rec[0] == 0 {
            break;
        }
        if rec[0..11] == field {
            return Some((
                u16::from_le_bytes([rec[26], rec[27]]),
                u32::from_le_bytes([rec[28], rec[29], rec[30], rec[31]]),
                rec[11],
            ));
        }
    }
    None
}

#[test]
fn a_fat16_volume_is_actually_fat16_and_the_files_are_where_the_directory_says() {
    let payload: Vec<u8> = (0..70_000u32).map(|i| (i % 251) as u8).collect();
    let img = fat16(
        &[
            FatFile {
                path: "EFI/BOOT/BOOTX64.EFI".into(),
                bytes: b"MZ this is the loader".to_vec(),
            },
            FatFile {
                path: "vmlinuz".into(),
                bytes: payload.clone(),
            },
            FatFile {
                path: "korp/cmdline".into(),
                bytes: b"console=ttyS0".to_vec(),
            },
        ],
        "GUNNAR",
    )
    .expect("a legal FAT16 volume");

    let bpb = parse_bpb(&img);
    assert_eq!(bpb.bytes_per_sector, 512);
    assert_eq!(img[510], 0x55);
    assert_eq!(img[511], 0xAA, "the boot sector signature is missing");
    assert_eq!(&img[54..62], b"FAT16   ");
    assert_eq!(
        img.len(),
        bpb.total_sectors * bpb.bytes_per_sector,
        "the image is not the size its own BPB declares"
    );

    // **The FAT type is derived, not declared.** `"FAT16   "` at offset 0x36 is
    // documentation; a driver computes the type from the cluster count, and a
    // volume under 4085 clusters is FAT12 however it is labelled. This is the
    // assertion the label cannot make.
    let root_sectors = (bpb.root_entries * 32) / bpb.bytes_per_sector;
    let data_start = bpb.reserved + bpb.num_fats * bpb.sectors_per_fat + root_sectors;
    let clusters = (bpb.total_sectors - data_start) / bpb.sectors_per_cluster;
    assert!(
        (4085..=65_524).contains(&clusters),
        "{clusters} clusters is not FAT16: under 4085 a driver reads the FAT as 12-bit and \
         over 65524 as FAT32, and the label at 0x36 changes neither"
    );

    let root_at = data_start.saturating_sub(root_sectors) * bpb.bytes_per_sector;
    let root = &img[root_at..root_at + bpb.root_entries * 32];

    // The kernel, in the root, with the size the file actually has.
    let (cluster, size, attr) = find_entry(root, "vmlinuz").expect("vmlinuz in the root directory");
    assert_eq!(size as usize, payload.len());
    assert_eq!(attr & 0x10, 0, "vmlinuz must not be marked as a directory");
    let at = (data_start + (cluster as usize - 2) * bpb.sectors_per_cluster) * bpb.bytes_per_sector;
    assert_eq!(
        &img[at..at + payload.len()],
        &payload[..],
        "the bytes at the cluster the directory entry points to are not the file"
    );

    // EFI/ is a directory, BOOT/ is inside it, and BOOTX64.EFI is inside that.
    // Reached by walking, because a loader that wrote every entry into the root
    // and every directory record correctly would pass a flat check.
    let (efi_cluster, _, attr) = find_entry(root, "EFI").expect("EFI/ in the root");
    assert_eq!(attr & 0x10, 0x10, "EFI must be marked as a directory");
    let efi_at =
        (data_start + (efi_cluster as usize - 2) * bpb.sectors_per_cluster) * bpb.bytes_per_sector;
    let efi = &img[efi_at..efi_at + bpb.sectors_per_cluster * bpb.bytes_per_sector];
    assert_eq!(
        &efi[0..11],
        b".          ",
        "a subdirectory's first entry must be `.`"
    );
    assert_eq!(
        &efi[32..43],
        b"..         ",
        "a subdirectory's second entry must be `..`"
    );
    let (boot_cluster, _, _) = find_entry(efi, "BOOT").expect("BOOT/ inside EFI/");
    let boot_at =
        (data_start + (boot_cluster as usize - 2) * bpb.sectors_per_cluster) * bpb.bytes_per_sector;
    let boot = &img[boot_at..boot_at + bpb.sectors_per_cluster * bpb.bytes_per_sector];
    let (loader_cluster, loader_size, _) =
        find_entry(boot, "BOOTX64.EFI").expect("BOOTX64.EFI inside EFI/BOOT/");
    let loader_at = (data_start + (loader_cluster as usize - 2) * bpb.sectors_per_cluster)
        * bpb.bytes_per_sector;
    assert_eq!(
        &img[loader_at..loader_at + loader_size as usize],
        b"MZ this is the loader",
        "the firmware would load these bytes as EFI/BOOT/BOOTX64.EFI"
    );

    // Both FAT copies must agree; a driver is entitled to read either.
    let fat0 = bpb.reserved * bpb.bytes_per_sector;
    let fat1 = (bpb.reserved + bpb.sectors_per_fat) * bpb.bytes_per_sector;
    let len = bpb.sectors_per_fat * bpb.bytes_per_sector;
    assert_eq!(
        &img[fat0..fat0 + len],
        &img[fat1..fat1 + len],
        "the two FAT copies differ"
    );
    assert_eq!(
        u16::from_le_bytes([img[fat0], img[fat0 + 1]]),
        0xFFF8,
        "FAT entry 0 must be the media descriptor"
    );
}

#[test]
fn a_file_spanning_several_clusters_has_a_chain_that_ends() {
    // 70 000 bytes at any legal cluster size is more than one cluster, so this
    // exercises the chain rather than the single-cluster case every small file
    // takes.
    let payload: Vec<u8> = (0..70_000u32).map(|i| (i % 97) as u8).collect();
    let img = fat16(
        &[FatFile {
            path: "vmlinuz".into(),
            bytes: payload.clone(),
        }],
        "GUNNAR",
    )
    .expect("a legal volume");
    let bpb = parse_bpb(&img);
    let root_sectors = (bpb.root_entries * 32) / bpb.bytes_per_sector;
    let data_start = bpb.reserved + bpb.num_fats * bpb.sectors_per_fat + root_sectors;
    let root_at = (bpb.reserved + bpb.num_fats * bpb.sectors_per_fat) * bpb.bytes_per_sector;
    let root = &img[root_at..root_at + bpb.root_entries * 32];
    let (first, size, _) = find_entry(root, "vmlinuz").expect("vmlinuz");
    assert_eq!(size as usize, payload.len());

    let fat_at = bpb.reserved * bpb.bytes_per_sector;
    let entry = |c: u16| {
        u16::from_le_bytes([
            img[fat_at + c as usize * 2],
            img[fat_at + c as usize * 2 + 1],
        ])
    };

    let mut chain = vec![first];
    let mut c = first;
    // A cycle here would hang a real FAT driver, so the walk is bounded and the
    // bound is itself an assertion.
    for _ in 0..1000 {
        let n = entry(c);
        if n >= 0xFFF8 {
            break;
        }
        assert!(n >= 2, "cluster {c} points at a reserved entry {n}");
        chain.push(n);
        c = n;
    }
    assert_eq!(
        entry(c),
        0xFFFF,
        "the last cluster of the chain must be an end-of-chain marker"
    );
    let cluster_bytes = bpb.sectors_per_cluster * bpb.bytes_per_sector;
    assert_eq!(
        chain.len(),
        payload.len().div_ceil(cluster_bytes),
        "the chain is not as long as the file needs"
    );
    // And the bytes really are in those clusters, in order.
    let mut got = Vec::new();
    for c in &chain {
        let at = (data_start + (*c as usize - 2) * bpb.sectors_per_cluster) * bpb.bytes_per_sector;
        got.extend_from_slice(&img[at..at + cluster_bytes]);
    }
    assert_eq!(&got[..payload.len()], &payload[..]);
}

// ── ISO-9660 + El Torito ────────────────────────────────────────────────────

#[test]
fn the_iso_carries_the_three_volume_descriptors_and_a_boot_catalog() {
    let esp = vec![0xABu8; 4096];
    let iso = iso9660_eltorito("GUNNAR_APPLIANCE", "EFI.IMG", &esp).expect("an ISO");

    // The system area is 32 KiB and must be untouched: it is where an isohybrid
    // MBR or GPT would go, and writing into it is how an ISO stops being
    // convertible to a disk image later.
    assert!(
        iso[..16 * 2048].iter().all(|b| *b == 0),
        "the 32 KiB system area is not empty"
    );

    let pvd = &iso[16 * 2048..17 * 2048];
    assert_eq!(pvd[0], 1, "sector 16 must be the primary volume descriptor");
    assert_eq!(&pvd[1..6], b"CD001");
    assert_eq!(&pvd[40..56], b"GUNNAR_APPLIANCE");
    // Both-endian: a reader is entitled to consult either half.
    let space_le = u32::from_le_bytes(pvd[80..84].try_into().unwrap());
    let space_be = u32::from_be_bytes(pvd[84..88].try_into().unwrap());
    assert_eq!(space_le, space_be, "the both-endian halves disagree");
    assert_eq!(
        space_le as usize * 2048,
        iso.len(),
        "the volume space size does not describe the file"
    );

    let br = &iso[17 * 2048..18 * 2048];
    assert_eq!(
        br[0], 0,
        "sector 17 must be a boot record volume descriptor"
    );
    assert_eq!(&br[7..30], b"EL TORITO SPECIFICATION");
    let catalog_lba = u32::from_le_bytes(br[71..75].try_into().unwrap());

    let term = &iso[18 * 2048..19 * 2048];
    assert_eq!(term[0], 255, "the descriptor set must be terminated");

    // The catalog, found by following the pointer rather than by knowing where
    // it was put.
    let at = catalog_lba as usize * 2048;
    let cat = &iso[at..at + 64];
    assert_eq!(cat[0], 0x01, "the validation entry header id must be 1");
    assert_eq!(cat[1], 0xEF, "the platform id must be EFI");
    assert_eq!((cat[30], cat[31]), (0x55, 0xAA));
    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]]));
    }
    assert_eq!(
        sum, 0,
        "the validation entry checksum is wrong, and firmware ignores a bad catalog SILENTLY"
    );

    assert_eq!(cat[32], 0x88, "the boot entry must be marked bootable");
    assert_eq!(cat[33], 0x00, "no emulation");
    let sectors = u16::from_le_bytes([cat[38], cat[39]]) as usize;
    let boot_lba = u32::from_le_bytes(cat[40..44].try_into().unwrap()) as usize;
    // The trap this exists for: `sector_count` is 512-byte sectors, and the ISO
    // block is 2048. Getting it wrong exposes a partition four times too small
    // and the failure reads as "no bootable device".
    assert_eq!(
        sectors * 512,
        esp.len(),
        "sector_count does not cover the whole boot image"
    );
    assert_eq!(
        &iso[boot_lba * 2048..boot_lba * 2048 + esp.len()],
        &esp[..],
        "the LBA in the catalog does not point at the boot image"
    );
}

#[test]
fn the_boot_image_is_also_a_file_in_the_root_directory() {
    let esp = vec![0x5Au8; 8192];
    let iso = iso9660_eltorito("GUNNAR_APPLIANCE", "EFI.IMG", &esp).expect("an ISO");
    let pvd = &iso[16 * 2048..17 * 2048];
    // The root directory record lives inside the PVD at offset 156.
    let root_lba = u32::from_le_bytes(pvd[158..162].try_into().unwrap()) as usize;
    let dir = &iso[root_lba * 2048..(root_lba + 1) * 2048];

    // Walk the records: `.`, `..`, then the file.
    let mut at = 0usize;
    let mut names = Vec::new();
    while dir[at] != 0 {
        let len = dir[at] as usize;
        let id_len = dir[at + 32] as usize;
        names.push(String::from_utf8_lossy(&dir[at + 33..at + 33 + id_len]).into_owned());
        at += len;
    }
    assert_eq!(
        names.len(),
        3,
        "expected `.`, `..` and one file, got {names:?}"
    );
    assert_eq!(
        names[2], "EFI.IMG;1",
        "an ISO-9660 file identifier carries its version suffix"
    );
}

#[test]
fn an_oversized_boot_image_is_refused_rather_than_silently_truncated() {
    // 34 MiB: past what a u16 count of 512-byte sectors can describe. The
    // firmware would expose a partition that stops in the middle of the FAT and
    // report "no bootable device", so this has to be a build failure.
    let huge = vec![0u8; 34 * 1024 * 1024];
    let err = iso9660_eltorito("GUNNAR", "EFI.IMG", &huge)
        .expect_err("an ISO that cannot describe its own boot image must not be produced");
    assert!(
        err.contains("sector_count"),
        "the error must name the field that cannot hold the value: {err}"
    );
}

#[test]
fn the_catalog_checksum_is_recomputed_and_not_copied() {
    // Two different boot images must produce two different, individually valid
    // catalogs. A checksum computed once and pasted passes the first and fails
    // the second.
    for len in [2048usize, 1_048_576, 16_777_216] {
        let cat = eltorito_catalog(23, len);
        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]]));
        }
        assert_eq!(
            sum, 0,
            "the validation checksum is wrong for a {len}-byte image"
        );
        assert_eq!(
            u16::from_le_bytes([cat[38], cat[39]]) as usize * 512,
            len.next_multiple_of(512),
            "sector_count must round up, or the last partial sector is unreachable"
        );
    }
}