fastboop-core 0.0.1-rc.21

Core profile matching and boot orchestration primitives for fastboop.
Documentation
extern crate alloc;

use alloc::vec::Vec;

use fastboop_schema::{BootProfile, DeviceProfile};
use serde::{Deserialize, Serialize};

use crate::channel_stream::{CHANNEL_PROFILE_BUNDLE_FORMAT_V1, CHANNEL_PROFILE_BUNDLE_MAGIC};

pub const CHANNEL_PROFILE_BUNDLE_HEADER_LEN: usize = 6;

#[derive(Clone, Debug, Default)]
pub struct ChannelProfileBundle {
    pub devprofiles: Vec<DeviceProfile>,
    pub bootprofiles: Vec<BootProfile>,
}

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

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

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

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
struct ChannelProfileBundleV1Bin {
    devprofiles: Vec<DeviceProfile>,
    bootprofiles: Vec<BootProfile>,
}

pub fn decode_channel_profile_bundle(
    bytes: &[u8],
) -> Result<ChannelProfileBundle, ChannelProfileBundleCodecError> {
    let Some(format_version) = channel_profile_bundle_header_version(bytes) else {
        return Err(ChannelProfileBundleCodecError::InvalidMagic);
    };
    if format_version != CHANNEL_PROFILE_BUNDLE_FORMAT_V1 {
        return Err(ChannelProfileBundleCodecError::UnsupportedFormatVersion(
            format_version,
        ));
    }

    let payload = &bytes[CHANNEL_PROFILE_BUNDLE_HEADER_LEN..];
    let payload: ChannelProfileBundleV1Bin = postcard::from_bytes(payload)?;
    Ok(ChannelProfileBundle {
        devprofiles: payload.devprofiles,
        bootprofiles: payload.bootprofiles,
    })
}

pub fn encode_channel_profile_bundle(
    bundle: &ChannelProfileBundle,
) -> Result<Vec<u8>, postcard::Error> {
    let payload = postcard::to_allocvec(&ChannelProfileBundleV1Bin {
        devprofiles: bundle.devprofiles.clone(),
        bootprofiles: bundle.bootprofiles.clone(),
    })?;
    let mut out = Vec::with_capacity(CHANNEL_PROFILE_BUNDLE_HEADER_LEN + payload.len());
    out.extend_from_slice(&CHANNEL_PROFILE_BUNDLE_MAGIC);
    out.extend_from_slice(&CHANNEL_PROFILE_BUNDLE_FORMAT_V1.to_le_bytes());
    out.extend_from_slice(&payload);
    Ok(out)
}

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn roundtrips_empty_bundle() {
        let bundle = ChannelProfileBundle::default();
        let encoded = encode_channel_profile_bundle(&bundle).unwrap();
        let decoded = decode_channel_profile_bundle(&encoded).unwrap();
        assert!(decoded.devprofiles.is_empty());
        assert!(decoded.bootprofiles.is_empty());
    }

    #[test]
    fn rejects_invalid_magic() {
        let err = decode_channel_profile_bundle(b"xxxx\x01\x00payload").unwrap_err();
        matches!(err, ChannelProfileBundleCodecError::InvalidMagic);
    }
}