Skip to main content

wire/message_refs/
kind.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Type-distinct advertised-ref discriminator and consume routing.
3
4use objects::object::{
5    MarkerName, ReservedRefNameError, StateId, SyntheticFrontierName, SyntheticFrontierNameError,
6    ThreadName, is_reserved_heddle_namespace,
7};
8use serde::{Deserialize, Serialize};
9
10use super::RefEntry;
11
12/// Type-distinct discriminator for an advertised ref.
13///
14/// Synthetic frontier roots must never be coerced into a [`ThreadName`] or
15/// [`MarkerName`]. Match on this enum at every consume/store/mirror site.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum RefKind {
19    Thread,
20    Marker,
21    SyntheticFrontierRoot,
22}
23
24impl RefKind {
25    /// Classify an advertised name.
26    ///
27    /// A well-formed synthetic frontier name is always
28    /// [`RefKind::SyntheticFrontierRoot`], even if a boolean `is_thread` flag
29    /// still claims otherwise. A reserved `heddle/` name that is not a
30    /// well-formed frontier root is also classified as synthetic so it cannot
31    /// fall through to the thread or marker arms (fail closed).
32    pub fn from_advertised_name(name: &str, advertised_as_thread: bool) -> Self {
33        if SyntheticFrontierName::looks_like(name) || is_reserved_heddle_namespace(name) {
34            return RefKind::SyntheticFrontierRoot;
35        }
36        if advertised_as_thread {
37            RefKind::Thread
38        } else {
39            RefKind::Marker
40        }
41    }
42
43    pub fn is_user_thread(self) -> bool {
44        matches!(self, RefKind::Thread)
45    }
46
47    pub fn is_marker(self) -> bool {
48        matches!(self, RefKind::Marker)
49    }
50}
51
52/// Typed view of an advertised ref. The synthetic arm never holds a
53/// [`ThreadName`] or [`MarkerName`].
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum AdvertisedRef {
56    Thread(ThreadName),
57    Marker(MarkerName),
58    SyntheticFrontier(SyntheticFrontierName),
59}
60
61/// Why an advertised ref could not be consumed as its declared kind.
62#[derive(Debug, Clone, thiserror::Error)]
63pub enum AdvertisedRefError {
64    #[error("{0}")]
65    Reserved(#[from] ReservedRefNameError),
66    #[error("{0}")]
67    Synthetic(#[from] SyntheticFrontierNameError),
68    #[error("ref '{name}' is reserved and cannot be consumed as a {kind:?}")]
69    KindMismatch { name: String, kind: RefKind },
70}
71
72impl RefEntry {
73    pub fn thread(name: impl Into<String>, state_id: StateId) -> Self {
74        Self {
75            name: name.into(),
76            state_id,
77            kind: RefKind::Thread,
78        }
79    }
80
81    pub fn marker(name: impl Into<String>, state_id: StateId) -> Self {
82        Self {
83            name: name.into(),
84            state_id,
85            kind: RefKind::Marker,
86        }
87    }
88
89    pub fn synthetic_frontier(name: SyntheticFrontierName, state_id: StateId) -> Self {
90        Self {
91            name: name.as_name(),
92            state_id,
93            kind: RefKind::SyntheticFrontierRoot,
94        }
95    }
96
97    pub fn from_advertised(
98        name: impl Into<String>,
99        state_id: StateId,
100        advertised_as_thread: bool,
101    ) -> Self {
102        let name = name.into();
103        let kind = RefKind::from_advertised_name(&name, advertised_as_thread);
104        Self {
105            name,
106            state_id,
107            kind,
108        }
109    }
110
111    pub fn is_user_thread(&self) -> bool {
112        self.kind.is_user_thread()
113    }
114
115    pub fn is_marker(&self) -> bool {
116        self.kind.is_marker()
117    }
118
119    /// Route this entry to its type-distinct consume form.
120    ///
121    /// [`RefKind::SyntheticFrontierRoot`] yields only a
122    /// [`SyntheticFrontierName`]. It never constructs a [`ThreadName`] or
123    /// [`MarkerName`].
124    pub fn advertised(&self) -> Result<AdvertisedRef, AdvertisedRefError> {
125        match self.kind {
126            RefKind::Thread => {
127                if is_reserved_heddle_namespace(&self.name) {
128                    return Err(AdvertisedRefError::KindMismatch {
129                        name: self.name.clone(),
130                        kind: self.kind,
131                    });
132                }
133                Ok(AdvertisedRef::Thread(ThreadName::try_new(
134                    self.name.clone(),
135                )?))
136            }
137            RefKind::Marker => {
138                if is_reserved_heddle_namespace(&self.name) {
139                    return Err(AdvertisedRefError::KindMismatch {
140                        name: self.name.clone(),
141                        kind: self.kind,
142                    });
143                }
144                Ok(AdvertisedRef::Marker(MarkerName::try_new(
145                    self.name.clone(),
146                )?))
147            }
148            RefKind::SyntheticFrontierRoot => Ok(AdvertisedRef::SyntheticFrontier(
149                SyntheticFrontierName::parse(&self.name)?,
150            )),
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use objects::object::{ChangeId, StateId, SyntheticFrontierName};
158
159    use super::*;
160
161    fn cid(last: u8) -> ChangeId {
162        let mut bytes = [0u8; 16];
163        bytes[15] = last;
164        ChangeId::from_bytes(bytes)
165    }
166
167    #[test]
168    fn advertised_synthetic_root_never_becomes_thread_or_marker_name() {
169        let change = cid(11);
170        let name = SyntheticFrontierName::new("main", change).unwrap();
171        let entry = RefEntry::from_advertised(name.as_name(), StateId::from_bytes([1; 32]), true);
172        assert_eq!(entry.kind, RefKind::SyntheticFrontierRoot);
173        match entry.advertised().expect("synthetic consume") {
174            AdvertisedRef::SyntheticFrontier(parsed) => assert_eq!(parsed, name),
175            AdvertisedRef::Thread(_) | AdvertisedRef::Marker(_) => {
176                panic!("synthetic root must not construct ThreadName or MarkerName")
177            }
178        }
179    }
180
181    #[test]
182    fn advertised_as_thread_flag_cannot_reclassify_a_frontier_root() {
183        let change = cid(12);
184        let name = SyntheticFrontierName::new("main", change).unwrap();
185        assert_eq!(
186            RefKind::from_advertised_name(&name.as_name(), true),
187            RefKind::SyntheticFrontierRoot
188        );
189        assert_eq!(
190            RefKind::from_advertised_name(&name.as_name(), false),
191            RefKind::SyntheticFrontierRoot
192        );
193        assert_eq!(
194            RefKind::from_advertised_name("heddle/not-a-frontier", true),
195            RefKind::SyntheticFrontierRoot
196        );
197    }
198
199    #[test]
200    fn user_thread_at_hd_suffix_stays_a_thread() {
201        let entry = RefEntry::from_advertised("main@hd-abcdef", StateId::from_bytes([2; 32]), true);
202        assert_eq!(entry.kind, RefKind::Thread);
203        match entry.advertised().expect("user thread") {
204            AdvertisedRef::Thread(name) => assert_eq!(name.as_str(), "main@hd-abcdef"),
205            other => panic!("expected thread, got {other:?}"),
206        }
207    }
208}