lodestone-types 0.0.16

Shared semantic types for Lodestone components and public Rust APIs
Documentation
//! Closed nonauthoritative executable identity for exact development supplies.

use std::error::Error;
use std::fmt::{self, Display, Formatter};

use serde::{Deserialize, Serialize};

pub const BUILD_IDENTITY_SCHEMA_ID: &str = "lodestone-build-identity/v1";
pub const BUILD_IDENTITY_FORMAT_VERSION: u16 = 1;
pub const MAX_BUILD_IDENTITY_BYTES: usize = 16 * 1024;

// INVARIANT: These portable crate-local projections are derived from the
// repository authorities by `check-repository-authorities.py`. Embedding the
// authority files with repository-relative paths would make the published
// `lodestone-types` crate impossible to compile outside this checkout.
const NATIVE_API_SHA256: &str = "sha256:3a14f2017b295839922ab36dd98b32e02dad1d314ecb9ccbc396a9c89cf84b5b";
const CONFIGURATION_SCHEMA_SHA256: &str = "sha256:5e80c4bddc11dc363a412143f8fb20fc0ace5a390fbc77ece36609099b23a39b";
const SUPPLY_CONTRACT_SHA256: &str = "sha256:2141026b1456de36051ebfb558eaea6e1c1c7f1566afae409deddc136c225d80";

/// Source binding carried by a product executable.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum BuildSourceBindingV1 {
    /// Ordinary source-tree builds make no package-source claim.
    Unbound,
    /// Supply builds bind one exact clean revision and tree.
    ExactDevelopmentSupply {
        revision: String,
        tree: String,
        clean_tree: bool,
        source_state_sha256: String,
    },
}

impl BuildSourceBindingV1 {
    /// Projects the four all-or-none executable build inputs. A partial or
    /// malformed combination remains representable only as an invalid exact
    /// binding so the ordinary identity validation path refuses it.
    #[must_use]
    pub fn from_executable_environment(
        revision: Option<&str>,
        tree: Option<&str>,
        clean_tree: Option<&str>,
        source_state_sha256: Option<&str>,
    ) -> Self {
        match (revision, tree, clean_tree, source_state_sha256) {
            (None, None, None, None) => Self::Unbound,
            (Some(revision), Some(tree), Some("true"), Some(source_state_sha256)) => Self::ExactDevelopmentSupply {
                revision: revision.to_owned(),
                tree: tree.to_owned(),
                clean_tree: true,
                source_state_sha256: source_state_sha256.to_owned(),
            },
            (revision, tree, clean_tree, source_state_sha256) => Self::ExactDevelopmentSupply {
                revision: revision.unwrap_or_default().to_owned(),
                tree: tree.unwrap_or_default().to_owned(),
                clean_tree: clean_tree == Some("true"),
                source_state_sha256: source_state_sha256.unwrap_or_default().to_owned(),
            },
        }
    }
}

/// Closed executable projection compared with the outer supply index.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct BuildIdentityV1 {
    pub schema_id: String,
    pub format_version: u16,
    pub artifact_version: String,
    pub target: String,
    pub native_api_sha256: String,
    pub configuration_schema_sha256: String,
    pub supply_contract_sha256: String,
    pub source_binding: BuildSourceBindingV1,
}

impl BuildIdentityV1 {
    /// Constructs the ordinary unbound identity for this artifact target.
    #[must_use]
    pub fn current() -> Self {
        Self::current_with_source_binding(BuildSourceBindingV1::Unbound)
    }

    /// Constructs this artifact target's identity with a leaf-executable
    /// source binding. Keeping revision-specific values at executable leaves
    /// prevents them from invalidating every foundational workspace crate.
    #[must_use]
    pub fn current_with_source_binding(source_binding: BuildSourceBindingV1) -> Self {
        Self {
            schema_id: BUILD_IDENTITY_SCHEMA_ID.to_owned(),
            format_version: BUILD_IDENTITY_FORMAT_VERSION,
            artifact_version: env!("CARGO_PKG_VERSION").to_owned(),
            target: env!("LODESTONE_COMPILE_TARGET").to_owned(),
            native_api_sha256: NATIVE_API_SHA256.to_owned(),
            configuration_schema_sha256: CONFIGURATION_SCHEMA_SHA256.to_owned(),
            supply_contract_sha256: SUPPLY_CONTRACT_SHA256.to_owned(),
            source_binding,
        }
    }

    /// Validates a decoded projection without trusting its producer.
    pub fn validate(&self) -> Result<(), BuildIdentityError> {
        if self.schema_id != BUILD_IDENTITY_SCHEMA_ID || self.format_version != BUILD_IDENTITY_FORMAT_VERSION {
            return Err(BuildIdentityError::UnsupportedFormat);
        }
        if !valid_artifact_version(&self.artifact_version) || !valid_target(&self.target) {
            return Err(BuildIdentityError::InvalidIdentity);
        }
        if !valid_sha256(&self.native_api_sha256)
            || !valid_sha256(&self.configuration_schema_sha256)
            || !valid_sha256(&self.supply_contract_sha256)
        {
            return Err(BuildIdentityError::InvalidDigest);
        }
        if let BuildSourceBindingV1::ExactDevelopmentSupply {
            revision,
            tree,
            clean_tree,
            source_state_sha256,
        } = &self.source_binding
            && (!valid_git_object(revision)
                || !valid_git_object(tree)
                || !clean_tree
                || !valid_sha256(source_state_sha256))
        {
            return Err(BuildIdentityError::InvalidSourceBinding);
        }
        Ok(())
    }

    /// Encodes the exact compact JSON command projection.
    pub fn encode_canonical(&self) -> Result<Vec<u8>, BuildIdentityError> {
        self.validate()?;
        // INVARIANT: Materialize through `Value` so every object, including the
        // internally tagged source binding, uses lexicographically ordered keys.
        // The independent supply verifier applies the same canonical JSON rule.
        let value = serde_json::to_value(self).map_err(|_| BuildIdentityError::Encoding)?;
        serde_json::to_vec(&value).map_err(|_| BuildIdentityError::Encoding)
    }

    /// Decodes one bounded canonical build-identity document.
    pub fn decode_canonical(bytes: &[u8]) -> Result<Self, BuildIdentityError> {
        if bytes.is_empty() || bytes.len() > MAX_BUILD_IDENTITY_BYTES {
            return Err(BuildIdentityError::InvalidSize);
        }
        let value: Self = serde_json::from_slice(bytes).map_err(|_| BuildIdentityError::Malformed)?;
        value.validate()?;
        if value.encode_canonical()?.as_slice() != bytes {
            return Err(BuildIdentityError::NonCanonical);
        }
        Ok(value)
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BuildIdentityError {
    InvalidSize,
    Malformed,
    NonCanonical,
    UnsupportedFormat,
    InvalidIdentity,
    InvalidDigest,
    InvalidSourceBinding,
    Encoding,
}

impl Display for BuildIdentityError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::InvalidSize => "build identity is empty or oversized",
            Self::Malformed => "build identity is malformed",
            Self::NonCanonical => "build identity is not canonical",
            Self::UnsupportedFormat => "build identity format is unsupported",
            Self::InvalidIdentity => "build identity contains an invalid artifact version or target",
            Self::InvalidDigest => "build identity contains an invalid digest",
            Self::InvalidSourceBinding => "build identity contains an invalid source binding",
            Self::Encoding => "build identity could not be encoded",
        })
    }
}

impl Error for BuildIdentityError {}

fn valid_sha256(value: &str) -> bool {
    value.len() == 71 && value.starts_with("sha256:") && lower_hex(value.trim_start_matches("sha256:"), 64, 64)
}

fn valid_git_object(value: &str) -> bool {
    lower_hex(value, 40, 64)
}

fn lower_hex(value: &str, minimum: usize, maximum: usize) -> bool {
    (minimum..=maximum).contains(&value.len())
        && value
            .as_bytes()
            .iter()
            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte))
}

fn valid_artifact_version(value: &str) -> bool {
    !value.is_empty()
        && value.len() <= 128
        && value
            .as_bytes()
            .iter()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'+' | b'-'))
}

fn valid_target(value: &str) -> bool {
    !value.is_empty()
        && value.len() <= 128
        && value
            .as_bytes()
            .first()
            .is_some_and(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'_')
        && value
            .as_bytes()
            .iter()
            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'.' | b'-'))
}

#[cfg(test)]
mod tests {
    use super::{BuildIdentityError, BuildIdentityV1, BuildSourceBindingV1};

    #[test]
    fn current_identity_is_valid_and_canonical() -> Result<(), Box<dyn std::error::Error>> {
        let identity = BuildIdentityV1::current();
        identity.validate()?;
        let encoded = identity.encode_canonical()?;
        assert_eq!(BuildIdentityV1::decode_canonical(&encoded)?, identity);
        assert!(matches!(identity.source_binding, BuildSourceBindingV1::Unbound));
        Ok(())
    }

    #[test]
    fn malformed_partial_and_noncanonical_documents_refuse() -> Result<(), Box<dyn std::error::Error>> {
        let identity = BuildIdentityV1::current();
        let mut value = serde_json::to_value(&identity)?;
        value["source_binding"] = serde_json::json!({
            "kind": "exact_development_supply",
            "revision": "1".repeat(40)
        });
        let partial = serde_json::to_vec(&value)?;
        assert_eq!(
            BuildIdentityV1::decode_canonical(&partial),
            Err(BuildIdentityError::Malformed)
        );

        let mut pretty = serde_json::to_vec_pretty(&identity)?;
        pretty.push(b'\n');
        assert_eq!(
            BuildIdentityV1::decode_canonical(&pretty),
            Err(BuildIdentityError::NonCanonical)
        );
        Ok(())
    }

    #[test]
    fn executable_source_binding_is_all_or_none_and_fail_closed() {
        assert_eq!(
            BuildSourceBindingV1::from_executable_environment(None, None, None, None),
            BuildSourceBindingV1::Unbound
        );
        let partial = BuildIdentityV1::current_with_source_binding(BuildSourceBindingV1::from_executable_environment(
            Some(&"1".repeat(40)),
            None,
            Some("true"),
            None,
        ));
        assert_eq!(partial.validate(), Err(BuildIdentityError::InvalidSourceBinding));
    }

    #[test]
    fn package_and_self_digests_are_absent() -> Result<(), Box<dyn std::error::Error>> {
        let encoded = BuildIdentityV1::current().encode_canonical()?;
        let text = std::str::from_utf8(&encoded)?;
        assert!(!text.contains("package_digest"));
        assert!(!text.contains("executable_sha256"));
        assert!(!text.contains("oci_manifest"));
        Ok(())
    }

    #[test]
    fn canonical_encoding_orders_every_object_lexicographically() -> Result<(), Box<dyn std::error::Error>> {
        let identity = BuildIdentityV1 {
            schema_id: "lodestone-build-identity/v1".to_owned(),
            format_version: 1,
            artifact_version: env!("CARGO_PKG_VERSION").to_owned(),
            target: "x86_64-unknown-linux-musl".to_owned(),
            native_api_sha256: format!("sha256:{}", "1".repeat(64)),
            configuration_schema_sha256: format!("sha256:{}", "2".repeat(64)),
            supply_contract_sha256: format!("sha256:{}", "3".repeat(64)),
            source_binding: BuildSourceBindingV1::ExactDevelopmentSupply {
                revision: "4".repeat(40),
                tree: "5".repeat(40),
                clean_tree: true,
                source_state_sha256: format!("sha256:{}", "6".repeat(64)),
            },
        };

        let encoded = identity.encode_canonical()?;
        let expected = format!(
            concat!(
                r#"{{"artifact_version":"{artifact_version}","configuration_schema_sha256":"sha256:{configuration}","format_version":1,"native_api_sha256":"sha256:{api}","schema_id":"lodestone-build-identity/v1","source_binding":{{"clean_tree":true,"kind":"exact_development_supply","revision":"{revision}","source_state_sha256":"sha256:{source_state}","tree":"{tree}"}},"supply_contract_sha256":"sha256:{contract}","target":"x86_64-unknown-linux-musl"}}"#
            ),
            artifact_version = env!("CARGO_PKG_VERSION"),
            configuration = "2".repeat(64),
            api = "1".repeat(64),
            revision = "4".repeat(40),
            source_state = "6".repeat(64),
            tree = "5".repeat(40),
            contract = "3".repeat(64),
        );
        assert_eq!(encoded, expected.as_bytes());
        assert_eq!(BuildIdentityV1::decode_canonical(&encoded)?, identity);
        Ok(())
    }
}