fastboop-core 0.0.1-rc.21

Core profile matching and boot orchestration primitives for fastboop.
Documentation
use alloc::string::{String, ToString};
use alloc::vec::Vec;

use fastboop_schema::bin::{
    BOOT_PROFILE_BIN_FORMAT_V0, BOOT_PROFILE_BIN_V0_HEADER_LEN, BOOT_PROFILE_BIN_V0_MAGIC,
    BootProfileBin,
};
use fastboop_schema::{
    BootProfile, BootProfileArtifactPathSource, BootProfileRootfs,
    BootProfileRootfsFilesystemSource, InjectMac,
};
use gibblox_pipeline::{PipelineValidationError, validate_pipeline};

#[derive(Debug)]
pub enum BootProfileCodecError {
    Decode(postcard::Error),
    InvalidMagic,
    UnsupportedFormatVersion(u16),
}

impl core::fmt::Display for BootProfileCodecError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::Decode(err) => write!(f, "decode boot profile: {err}"),
            Self::InvalidMagic => {
                write!(
                    f,
                    "invalid boot profile magic (expected {BOOT_PROFILE_BIN_V0_MAGIC:?})"
                )
            }
            Self::UnsupportedFormatVersion(version) => {
                write!(
                    f,
                    "unsupported boot profile format version {version} (expected {BOOT_PROFILE_BIN_FORMAT_V0})"
                )
            }
        }
    }
}

impl From<postcard::Error> for BootProfileCodecError {
    fn from(err: postcard::Error) -> Self {
        Self::Decode(err)
    }
}

pub fn decode_boot_profile(bytes: &[u8]) -> Result<BootProfile, BootProfileCodecError> {
    let Some(format_version) = boot_profile_bin_header_version(bytes) else {
        return Err(BootProfileCodecError::InvalidMagic);
    };
    if format_version != BOOT_PROFILE_BIN_FORMAT_V0 {
        return Err(BootProfileCodecError::UnsupportedFormatVersion(
            format_version,
        ));
    }

    let payload = &bytes[BOOT_PROFILE_BIN_V0_HEADER_LEN..];
    let profile: BootProfileBin = postcard::from_bytes(payload)?;
    Ok(BootProfile::from(profile))
}

pub fn decode_boot_profile_prefix(
    bytes: &[u8],
) -> Result<(BootProfile, usize), BootProfileCodecError> {
    let Some(format_version) = boot_profile_bin_header_version(bytes) else {
        return Err(BootProfileCodecError::InvalidMagic);
    };
    if format_version != BOOT_PROFILE_BIN_FORMAT_V0 {
        return Err(BootProfileCodecError::UnsupportedFormatVersion(
            format_version,
        ));
    }

    let payload = &bytes[BOOT_PROFILE_BIN_V0_HEADER_LEN..];
    let (profile, remaining): (BootProfileBin, &[u8]) = postcard::take_from_bytes(payload)?;
    let consumed = BOOT_PROFILE_BIN_V0_HEADER_LEN
        .checked_add(payload.len() - remaining.len())
        .expect("boot profile consumed length overflow");
    Ok((BootProfile::from(profile), consumed))
}

pub fn encode_boot_profile(profile: &BootProfile) -> Result<Vec<u8>, postcard::Error> {
    let payload = postcard::to_allocvec(&BootProfileBin::from(profile.clone()))?;
    let mut out = Vec::with_capacity(BOOT_PROFILE_BIN_V0_HEADER_LEN + payload.len());
    out.extend_from_slice(&BOOT_PROFILE_BIN_V0_MAGIC);
    out.extend_from_slice(&BOOT_PROFILE_BIN_FORMAT_V0.to_le_bytes());
    out.extend_from_slice(&payload);
    Ok(out)
}

pub fn boot_profile_bin_header_version(bytes: &[u8]) -> Option<u16> {
    if bytes.len() < BOOT_PROFILE_BIN_V0_HEADER_LEN {
        return None;
    }
    if bytes[..BOOT_PROFILE_BIN_V0_MAGIC.len()] != BOOT_PROFILE_BIN_V0_MAGIC {
        return None;
    }
    Some(u16::from_le_bytes([
        bytes[BOOT_PROFILE_BIN_V0_MAGIC.len()],
        bytes[BOOT_PROFILE_BIN_V0_MAGIC.len() + 1],
    ]))
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EffectiveBootProfileStage0 {
    pub dt_overlays: Vec<Vec<u8>>,
    pub extra_cmdline: Option<String>,
    pub kernel_modules: Vec<String>,
    pub inject_mac: Option<InjectMac>,
}

pub fn resolve_effective_boot_profile_stage0(
    profile: &BootProfile,
    device_profile_id: &str,
) -> EffectiveBootProfileStage0 {
    let mut dt_overlays = profile.dt_overlays.clone();
    let mut kernel_modules = profile.stage0.kernel_modules.clone();
    let mut extra_cmdline = profile.extra_cmdline.clone();
    let mut inject_mac = None;

    if let Some(device) = profile.stage0.devices.get(device_profile_id) {
        dt_overlays.extend(device.dt_overlays.iter().cloned());
        kernel_modules.extend(device.stage0.kernel_modules.iter().cloned());
        extra_cmdline =
            join_cmdline_parts(extra_cmdline.as_deref(), device.extra_cmdline.as_deref());
        inject_mac = device.stage0.inject_mac.clone();
    }

    EffectiveBootProfileStage0 {
        dt_overlays,
        extra_cmdline,
        kernel_modules,
        inject_mac,
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BootProfileValidationError {
    UnsupportedRootfsFilesystem { filesystem: &'static str },
    Pipeline(PipelineValidationError),
    EmptyKernelPath,
    EmptyDtbsPath,
}

impl core::fmt::Display for BootProfileValidationError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::UnsupportedRootfsFilesystem { filesystem } => write!(
                f,
                "boot profile rootfs filesystem '{filesystem}' is not supported for stage0 switchroot (supported: erofs, ext4)"
            ),
            Self::Pipeline(err) => write!(f, "{err}"),
            Self::EmptyKernelPath => {
                write!(f, "boot profile kernel path must not be empty")
            }
            Self::EmptyDtbsPath => {
                write!(f, "boot profile dtbs path must not be empty")
            }
        }
    }
}

pub fn validate_boot_profile(profile: &BootProfile) -> Result<(), BootProfileValidationError> {
    if !rootfs_supports_stage0_switchroot(&profile.rootfs) {
        return Err(BootProfileValidationError::UnsupportedRootfsFilesystem { filesystem: "fat" });
    }
    validate_pipeline(profile.rootfs.source()).map_err(BootProfileValidationError::Pipeline)?;
    if let Some(kernel) = profile.kernel.as_ref() {
        validate_profile_artifact_path_source(kernel, BootProfileValidationError::EmptyKernelPath)?;
    }
    if let Some(dtbs) = profile.dtbs.as_ref() {
        validate_profile_artifact_path_source(dtbs, BootProfileValidationError::EmptyDtbsPath)?;
    }
    Ok(())
}

fn rootfs_supports_stage0_switchroot(rootfs: &BootProfileRootfs) -> bool {
    match rootfs {
        BootProfileRootfs::Erofs(_) | BootProfileRootfs::Ext4(_) => true,
        BootProfileRootfs::Fat(_) => false,
        BootProfileRootfs::Ostree(source) => matches!(
            &source.ostree,
            BootProfileRootfsFilesystemSource::Erofs(_)
                | BootProfileRootfsFilesystemSource::Ext4(_)
        ),
    }
}

fn join_cmdline_parts(primary: Option<&str>, secondary: Option<&str>) -> Option<String> {
    let first = primary.map(str::trim).filter(|value| !value.is_empty());
    let second = secondary.map(str::trim).filter(|value| !value.is_empty());
    match (first, second) {
        (Some(a), Some(b)) => Some([a, b].join(" ")),
        (Some(a), None) => Some(a.to_string()),
        (None, Some(b)) => Some(b.to_string()),
        (None, None) => None,
    }
}

fn validate_profile_artifact_path_source(
    source: &BootProfileArtifactPathSource,
    empty_path_err: BootProfileValidationError,
) -> Result<(), BootProfileValidationError> {
    if source.path.trim().is_empty() {
        return Err(empty_path_err);
    }
    validate_pipeline(source.artifact_source()).map_err(BootProfileValidationError::Pipeline)
}