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";
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LocalDevelopmentInstanceId([u8; 32]);
impl LocalDevelopmentInstanceId {
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)
}
pub fn from_be_bytes(bytes: [u8; 32]) -> Result<Self, LocalDevelopmentInstanceIdError> {
if bytes == [0; 32] {
Err(LocalDevelopmentInstanceIdError)
} else {
Ok(Self(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)
}
}
#[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(())
}
}