use std::collections::BTreeMap; use std::fmt; use serde::{Deserialize, Serialize}; pub const MAX_VALUE_DEPTH: usize = 32; pub const MAX_VALUE_NODES: usize = 4096; pub const MAX_VALUE_BYTES: usize = 1024 * 1024; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(try_from = "String", into = "String")] pub struct ActorRef([u8; 21]); impl ActorRef { pub fn new() -> Self { Self::parse(&nanoid::nanoid!()).expect("Nano ID uses the supported alphabet") } pub fn as_str(&self) -> &str { std::str::from_utf8(&self.0).expect("Nano ID is valid UTF-8") } pub fn parse(value: &str) -> Option { if value.len() != 21 || !value .bytes() .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-')) { return None; } let mut bytes = [0; 21]; bytes.copy_from_slice(value.as_bytes()); Some(Self(bytes)) } } impl Default for ActorRef { fn default() -> Self { Self::new() } } impl TryFrom for ActorRef { type Error = String; fn try_from(value: String) -> Result { Self::parse(&value) .ok_or_else(|| "actor reference must be a 21-character URL-safe NanoID".to_owned()) } } impl From for String { fn from(value: ActorRef) -> Self { value.to_string() } } impl fmt::Debug for ActorRef { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "@{}", self.as_str()) } } impl fmt::Display for ActorRef { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(self.as_str()) } } /// Owned actor data, never a shared Rune heap object or execution handle. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde( tag = "type", content = "data", rename_all = "snake_case", deny_unknown_fields )] pub enum Value { Null, Bool(bool), Integer(i64), String(String), List(Vec), Record(BTreeMap), ActorRef(ActorRef), } impl Value { pub fn validate(&self) -> Result<(), String> { fn visit( value: &Value, depth: usize, nodes: &mut usize, bytes: &mut usize, ) -> Result<(), String> { *nodes += 1; if depth > MAX_VALUE_DEPTH || *nodes > MAX_VALUE_NODES { return Err("actor value exceeds depth or node limit".to_owned()); } match value { Value::String(value) => *bytes += value.len(), Value::List(values) => { for value in values { visit(value, depth + 1, nodes, bytes)?; } } Value::Record(values) => { for (key, value) in values { *bytes += key.len(); visit(value, depth + 1, nodes, bytes)?; } } _ => {} } if *bytes > MAX_VALUE_BYTES { return Err("actor value exceeds string byte limit".to_owned()); } Ok(()) } visit(self, 0, &mut 0, &mut 0) } } #[cfg(test)] mod tests { use super::*; #[test] fn generated_references_round_trip_and_are_distinct_values() { let reference = ActorRef::new(); assert_eq!(ActorRef::parse(reference.as_str()), Some(reference)); assert_eq!(format!("{reference:?}"), format!("@{reference}")); assert_ne!( Value::ActorRef(reference), Value::String(reference.to_string()) ); assert!(ActorRef::parse("too-short").is_none()); assert!(ActorRef::parse("abcdefghijklmnopqrst!").is_none()); } #[test] fn value_limits_are_inclusive() { let mut value = Value::Null; for _ in 0..MAX_VALUE_DEPTH { value = Value::List(vec![value]); } assert!(value.validate().is_ok()); assert!(Value::List(vec![value]).validate().is_err()); assert!( Value::List(vec![Value::Null; MAX_VALUE_NODES - 1]) .validate() .is_ok() ); assert!( Value::List(vec![Value::Null; MAX_VALUE_NODES]) .validate() .is_err() ); assert!( Value::String("x".repeat(MAX_VALUE_BYTES)) .validate() .is_ok() ); assert!( Value::String("x".repeat(MAX_VALUE_BYTES + 1)) .validate() .is_err() ); } }