Skip to main content

lodestone_types/
local_development.rs

1//! Semantic identity shared by local-development reference artifacts.
2
3use std::error::Error;
4use std::fmt::{self, Display, Formatter};
5use std::str::FromStr;
6
7use aws_lc_rs::digest;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10const DERIVATION_DOMAIN: &[u8] = b"lodestone/local-development/instance-id/v1\0";
11
12/// Non-secret 256-bit identity of one local-development instance lineage.
13#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct LocalDevelopmentInstanceId([u8; 32]);
15
16impl LocalDevelopmentInstanceId {
17    /// Domain-derives an instance identity from a workflow transaction identity.
18    pub fn derive_from_workflow_transaction(transaction_id: [u8; 32]) -> Result<Self, LocalDevelopmentInstanceIdError> {
19        if transaction_id == [0; 32] {
20            return Err(LocalDevelopmentInstanceIdError);
21        }
22        let mut context = digest::Context::new(&digest::SHA256);
23        context.update(DERIVATION_DOMAIN);
24        context.update(&transaction_id);
25        let observed = context.finish();
26        let mut bytes = [0_u8; 32];
27        bytes.copy_from_slice(observed.as_ref());
28        Self::from_be_bytes(bytes)
29    }
30
31    /// Constructs a nonzero identity from its fixed-width bytes.
32    pub fn from_be_bytes(bytes: [u8; 32]) -> Result<Self, LocalDevelopmentInstanceIdError> {
33        if bytes == [0; 32] {
34            Err(LocalDevelopmentInstanceIdError)
35        } else {
36            Ok(Self(bytes))
37        }
38    }
39
40    /// Returns the exact fixed-width identity bytes.
41    #[must_use]
42    pub const fn to_be_bytes(self) -> [u8; 32] {
43        self.0
44    }
45}
46
47impl Display for LocalDevelopmentInstanceId {
48    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
49        for byte in self.0 {
50            write!(formatter, "{byte:02x}")?;
51        }
52        Ok(())
53    }
54}
55
56impl FromStr for LocalDevelopmentInstanceId {
57    type Err = LocalDevelopmentInstanceIdError;
58
59    fn from_str(value: &str) -> Result<Self, Self::Err> {
60        if value.len() != 64
61            || value
62                .bytes()
63                .any(|byte| !byte.is_ascii_digit() && !(b'a'..=b'f').contains(&byte))
64        {
65            return Err(LocalDevelopmentInstanceIdError);
66        }
67        let mut bytes = [0_u8; 32];
68        for (index, pair) in value.as_bytes().as_chunks::<2>().0.iter().enumerate() {
69            let text = std::str::from_utf8(pair).map_err(|_| LocalDevelopmentInstanceIdError)?;
70            bytes[index] = u8::from_str_radix(text, 16).map_err(|_| LocalDevelopmentInstanceIdError)?;
71        }
72        Self::from_be_bytes(bytes)
73    }
74}
75
76impl Serialize for LocalDevelopmentInstanceId {
77    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
78        serializer.serialize_str(&self.to_string())
79    }
80}
81
82impl<'de> Deserialize<'de> for LocalDevelopmentInstanceId {
83    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
84        let value = String::deserialize(deserializer)?;
85        value.parse().map_err(serde::de::Error::custom)
86    }
87}
88
89/// Typed refusal for malformed or zero local-development identities.
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91pub struct LocalDevelopmentInstanceIdError;
92
93impl Display for LocalDevelopmentInstanceIdError {
94    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
95        formatter.write_str("local development instance identity must be nonzero lower-case 256-bit hex")
96    }
97}
98
99impl Error for LocalDevelopmentInstanceIdError {}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn workflow_derivation_is_domain_separated_and_canonical() -> Result<(), Box<dyn Error>> {
107        let value = LocalDevelopmentInstanceId::derive_from_workflow_transaction([7; 32])?;
108        assert_eq!(value.to_string().parse::<LocalDevelopmentInstanceId>()?, value);
109        assert_ne!(value.to_be_bytes(), [7; 32]);
110        assert!("0".repeat(64).parse::<LocalDevelopmentInstanceId>().is_err());
111        assert!("A".repeat(64).parse::<LocalDevelopmentInstanceId>().is_err());
112        Ok(())
113    }
114}