Skip to main content

palladium_actor/
path.rs

1use crate::errors::PathParseError;
2use std::fmt;
3
4// ── AddrHash ──────────────────────────────────────────────────────────────────
5
6/// 128-bit routing identifier for an actor.
7///
8/// Composed of:
9/// - 64 bits: BLAKE3 hash of the actor's canonical path.
10/// - 64 bits: Generation ID (monotonically increasing, assigned by engine).
11///
12/// Not directly constructable outside this crate — only `from_path` (deprecated)
13/// or the new generation-aware constructors produce valid instances.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
15#[repr(transparent)]
16pub struct AddrHash(pub(crate) u128);
17
18impl AddrHash {
19    /// Create a new `AddrHash` from a path and a generation ID.
20    pub fn new(path: &ActorPath, generation: u64) -> Self {
21        let digest = blake3::hash(path.as_str().as_bytes());
22        let path_hash = u64::from_le_bytes(digest.as_bytes()[..8].try_into().unwrap());
23        Self::from_parts(path_hash, generation)
24    }
25
26    /// Internal: assemble from raw path hash and generation.
27    pub fn from_parts(path_hash: u64, generation: u64) -> Self {
28        let val = (u128::from(path_hash) << 64) | u128::from(generation);
29        Self(val)
30    }
31
32    /// DEPRECATED: Use `AddrHash::new(path, gen)`.
33    /// Currently defaults generation to 0 for transition.
34    pub fn from_path(path: &ActorPath) -> Self {
35        Self::new(path, 0)
36    }
37
38    /// Derive an `AddrHash` from arbitrary bytes — used for synthetic addresses
39    /// (ask correlation channels, timer callbacks).  Uses 0 for generation.
40    pub fn synthetic(bytes: &[u8]) -> Self {
41        let digest = blake3::hash(bytes);
42        let path_hash = u64::from_le_bytes(digest.as_bytes()[..8].try_into().unwrap());
43        Self::from_parts(path_hash, 0)
44    }
45
46    pub fn path_hash(self) -> u64 {
47        (self.0 >> 64) as u64
48    }
49
50    pub fn generation(self) -> u64 {
51        (self.0 & 0xFFFFFFFF_FFFFFFFF) as u64
52    }
53
54    pub fn as_u128(self) -> u128 {
55        self.0
56    }
57}
58
59// ── ActorPath ─────────────────────────────────────────────────────────────────
60
61/// A hierarchical actor path, e.g. `/engine:abc/user/orders/processor-1`.
62///
63/// Paths must start with `/`, must not end with `/` (except for root `/`),
64/// and each segment may only contain ASCII alphanumerics plus `-`, `_`, `:`,
65/// and `.`.
66#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
67pub struct ActorPath(pub(crate) String);
68
69impl ActorPath {
70    pub fn root() -> Self {
71        Self("/".to_string())
72    }
73
74    pub fn parse(s: &str) -> Result<Self, PathParseError> {
75        if s.is_empty() {
76            return Err(PathParseError::Empty);
77        }
78        if !s.starts_with('/') {
79            return Err(PathParseError::MustStartWithSlash);
80        }
81        if s.len() > 1 && s.ends_with('/') {
82            return Err(PathParseError::TrailingSlash);
83        }
84        if s == "/" {
85            return Ok(Self::root());
86        }
87        for seg in s.split('/').skip(1) {
88            if seg.is_empty() {
89                return Err(PathParseError::EmptySegment);
90            }
91            if !seg
92                .chars()
93                .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | ':' | '.'))
94            {
95                return Err(PathParseError::InvalidSegment);
96            }
97        }
98        Ok(Self(s.to_owned()))
99    }
100
101    pub fn child(&self, name: &str) -> Result<Self, PathParseError> {
102        if name.is_empty() || name.contains('/') {
103            return Err(PathParseError::InvalidSegment);
104        }
105        if !name
106            .chars()
107            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | ':' | '.'))
108        {
109            return Err(PathParseError::InvalidSegment);
110        }
111        let next = if self.0 == "/" {
112            format!("/{name}")
113        } else {
114            format!("{}/{name}", self.0)
115        };
116        Self::parse(&next)
117    }
118
119    pub fn parent(&self) -> Option<Self> {
120        if self.0 == "/" {
121            return None;
122        }
123        let idx = self.0.rfind('/').expect("ActorPath always contains '/'");
124        if idx == 0 {
125            Some(Self::root())
126        } else {
127            Some(Self(self.0[..idx].to_owned()))
128        }
129    }
130
131    pub fn is_ancestor_of(&self, other: &Self) -> bool {
132        if self.0 == "/" {
133            return other.0 != "/";
134        }
135        if self.0 == other.0 {
136            return false;
137        }
138        let prefix = format!("{}/", self.0);
139        other.0.starts_with(&prefix)
140    }
141
142    pub fn as_str(&self) -> &str {
143        &self.0
144    }
145}
146
147impl fmt::Display for ActorPath {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(f, "{}", self.0)
150    }
151}