lodestone_types/
local_development.rs1use 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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct LocalDevelopmentInstanceId([u8; 32]);
15
16impl LocalDevelopmentInstanceId {
17 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 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 #[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#[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}