composefs-boot 0.7.0

Helpers for bootable composefs systems
Documentation
//! Boot entry writing and installation functionality.
//!
//! This module provides functions to write boot entries to the filesystem, handling both
//! Boot Loader Specification Type 1 entries (separate kernel/initrd files) and Type 2
//! Unified Kernel Images. It manages file placement, directory creation, and command line
//! argument injection for composefs boot scenarios.

use std::{
    fs::{create_dir_all, write},
    path::Path,
};

use anyhow::{Context, Result, ensure};

use composefs::{fsverity::FsVerityHashValue, repository::Repository};

use crate::{
    bootloader::{BootEntry, Type1Entry, Type2Entry},
    cmdline::ComposefsCmdline,
    uki,
};

/// Writes a Type 1 boot entry to the boot directory.
///
/// # Arguments
///
/// * `t1` - The Type 1 entry to write
/// * `bootdir` - Path to the boot directory
/// * `boot_subdir` - Optional subdirectory to prepend to paths
/// * `karg` - The composefs kernel argument (encodes format version, digest, and insecure flag)
/// * `cmdline_extra` - Additional kernel command line arguments
/// * `repo` - The composefs repository
pub fn write_t1_simple<ObjectID: FsVerityHashValue>(
    mut t1: Type1Entry<ObjectID>,
    bootdir: &Path,
    boot_subdir: Option<&str>,
    karg: &ComposefsCmdline<ObjectID>,
    cmdline_extra: &[&str],
    repo: &Repository<ObjectID>,
) -> Result<()> {
    let bootdir = if let Some(subdir) = boot_subdir {
        let subdir_path = Path::new(subdir);
        bootdir.join(subdir_path.strip_prefix("/").unwrap_or(subdir_path))
    } else {
        bootdir.to_path_buf()
    };

    let karg_str = karg.to_cmdline_arg();
    t1.entry.adjust_cmdline(Some(&karg_str), cmdline_extra);

    // Write the content before we write the loader entry
    for (filename, file) in &t1.files {
        let pathname = Path::new(filename.as_ref());
        let file_path = bootdir.join(pathname.strip_prefix(Path::new("/"))?);
        // SAFETY: what safety? :)
        create_dir_all(file_path.parent().unwrap())?;
        write(file_path, composefs::fs::read_file(file, repo)?)?;
    }

    // And now the loader entry itself
    let loader_entries = bootdir.join("loader/entries");
    create_dir_all(&loader_entries)?;
    let entry = loader_entries.join(t1.filename.as_ref());
    let entry_content = t1.entry.lines.join("\n") + "\n";
    write(entry, entry_content)?;
    Ok(())
}

/// Writes a Type 2 boot entry (UKI) to the boot directory.
///
/// Validates that the UKI's embedded composefs karg (`composefs=` or `composefs.digest=`)
/// matches one of the expected `acceptable_digests`.
///
/// # Arguments
///
/// * `t2` - The Type 2 entry to write
/// * `bootdir` - Path to the boot directory
/// * `acceptable_digests` - The composefs root object IDs the UKI may carry
/// * `repo` - The composefs repository
pub fn write_t2_simple<ObjectID: FsVerityHashValue>(
    t2: Type2Entry<ObjectID>,
    bootdir: &Path,
    acceptable_digests: &[&ObjectID],
    repo: &Repository<ObjectID>,
) -> Result<()> {
    let efi_linux = bootdir.join("EFI/Linux");
    create_dir_all(&efi_linux)?;
    let filename = efi_linux.join(t2.file_path);
    let content = composefs::fs::read_file(&t2.file, repo)?;
    let cmdline = uki::get_cmdline(&content)?;
    let parsed = ComposefsCmdline::<ObjectID>::from_cmdline(cmdline)
        .with_context(|| format!("parsing UKI .cmdline section: {cmdline:?}"))?
        .ok_or_else(|| {
            anyhow::anyhow!(
                "UKI .cmdline has no composefs karg (composefs= or composefs.digest=): {cmdline:?}"
            )
        })?;

    parsed.validate_digest(acceptable_digests.iter().copied())?;
    write(filename, content)?;
    Ok(())
}

/// Writes boot entry to the boot partition
///
/// # Arguments
///
/// * repo           - The composefs repository
/// * entry          - Boot entry variant to be written
/// * karg           - The composefs kernel argument (encodes format version, digest, and insecure
///   flag); used to build the `composefs=` or `composefs.digest=` cmdline argument
/// * boot_partition - Path to the boot partition/directory
/// * boot_subdir    - If `Some(path)`, the path is prepended to `initrd` and `linux` keys in the BLS entry
///
/// For example, if `boot_partition = "/boot"` and `boot_subdir = Some("1")` ,
/// the BLS entry will contain
///
/// ```text
/// linux /boot/1/<entry_id>/linux
/// initrd /boot/1/<entry_id>/initrd
/// ```
///
/// If `boot_partition = "/boot"` and `boot_subdir = None` , the BLS entry will contain
///
/// ```text
/// linux /<entry_id>/linux
/// initrd /<entry_id>/initrd
/// ```
///
/// * entry_id       - In case of a BLS entry, the name of file to be generated in `loader/entries`
/// * cmdline_extra  - Extra kernel command line arguments
///
pub fn write_boot_simple<ObjectID: FsVerityHashValue>(
    repo: &Repository<ObjectID>,
    entry: BootEntry<ObjectID>,
    karg: &ComposefsCmdline<ObjectID>,
    boot_partition: &Path,
    boot_subdir: Option<&str>,
    entry_id: Option<&str>,
    cmdline_extra: &[&str],
) -> Result<()> {
    match entry {
        BootEntry::Type1(mut t1) => {
            if let Some(name) = entry_id {
                t1.relocate(boot_subdir, name);
            }
            write_t1_simple(t1, boot_partition, boot_subdir, karg, cmdline_extra, repo)?;
        }
        BootEntry::Type2(mut t2) => {
            if let Some(name) = entry_id {
                t2.rename(name);
            }
            ensure!(cmdline_extra.is_empty(), "Can't add --cmdline args to UKIs");
            write_t2_simple(t2, boot_partition, &[karg.digest()], repo)?;
        }
        BootEntry::UsrLibModulesVmLinuz(entry) => {
            let mut t1 = entry.into_type1(entry_id)?;
            if let Some(name) = entry_id {
                t1.relocate(boot_subdir, name);
            }
            write_t1_simple(t1, boot_partition, boot_subdir, karg, cmdline_extra, repo)?;
        }
    };

    Ok(())
}