lodestone-types 0.0.16

Shared semantic types for Lodestone components and public Rust APIs
Documentation
//! Semantic identity shared by local-development reference artifacts.

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

use aws_lc_rs::digest;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

const DERIVATION_DOMAIN: &[u8] = b"lodestone/local-development/instance-id/v1\0";

/// Non-secret 256-bit identity of one local-development instance lineage.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LocalDevelopmentInstanceId([u8; 32]);

impl LocalDevelopmentInstanceId {
    /// Domain-derives an instance identity from a workflow transaction identity.
    pub fn derive_from_workflow_transaction(transaction_id: [u8; 32]) -> Result<Self, LocalDevelopmentInstanceIdError> {
        if transaction_id == [0; 32] {
            return Err(LocalDevelopmentInstanceIdError);
        }
        let mut context = digest::Context::new(&digest::SHA256);
        context.update(DERIVATION_DOMAIN);
        context.update(&transaction_id);
        let observed = context.finish();
        let mut bytes = [0_u8; 32];
        bytes.copy_from_slice(observed.as_ref());
        Self::from_be_bytes(bytes)
    }

    /// Constructs a nonzero identity from its fixed-width bytes.
    pub fn from_be_bytes(bytes: [u8; 32]) -> Result<Self, LocalDevelopmentInstanceIdError> {
        if bytes == [0; 32] {
            Err(LocalDevelopmentInstanceIdError)
        } else {
            Ok(Self(bytes))
        }
    }

    /// Returns the exact fixed-width identity bytes.
    #[must_use]
    pub const fn to_be_bytes(self) -> [u8; 32] {
        self.0
    }
}

impl Display for LocalDevelopmentInstanceId {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        for byte in self.0 {
            write!(formatter, "{byte:02x}")?;
        }
        Ok(())
    }
}

impl FromStr for LocalDevelopmentInstanceId {
    type Err = LocalDevelopmentInstanceIdError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        if value.len() != 64
            || value
                .bytes()
                .any(|byte| !byte.is_ascii_digit() && !(b'a'..=b'f').contains(&byte))
        {
            return Err(LocalDevelopmentInstanceIdError);
        }
        let mut bytes = [0_u8; 32];
        for (index, pair) in value.as_bytes().as_chunks::<2>().0.iter().enumerate() {
            let text = std::str::from_utf8(pair).map_err(|_| LocalDevelopmentInstanceIdError)?;
            bytes[index] = u8::from_str_radix(text, 16).map_err(|_| LocalDevelopmentInstanceIdError)?;
        }
        Self::from_be_bytes(bytes)
    }
}

impl Serialize for LocalDevelopmentInstanceId {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for LocalDevelopmentInstanceId {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let value = String::deserialize(deserializer)?;
        value.parse().map_err(serde::de::Error::custom)
    }
}

/// Typed refusal for malformed or zero local-development identities.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LocalDevelopmentInstanceIdError;

impl Display for LocalDevelopmentInstanceIdError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        formatter.write_str("local development instance identity must be nonzero lower-case 256-bit hex")
    }
}

impl Error for LocalDevelopmentInstanceIdError {}

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

    #[test]
    fn workflow_derivation_is_domain_separated_and_canonical() -> Result<(), Box<dyn Error>> {
        let value = LocalDevelopmentInstanceId::derive_from_workflow_transaction([7; 32])?;
        assert_eq!(value.to_string().parse::<LocalDevelopmentInstanceId>()?, value);
        assert_ne!(value.to_be_bytes(), [7; 32]);
        assert!("0".repeat(64).parse::<LocalDevelopmentInstanceId>().is_err());
        assert!("A".repeat(64).parse::<LocalDevelopmentInstanceId>().is_err());
        Ok(())
    }
}