Skip to main content

radixdb_catalog/
identity.rs

1use std::fmt;
2use std::str::FromStr;
3
4use crate::{CatalogError, CatalogResult};
5
6/// Stable opaque identity of one logical catalog object.
7#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct ObjectId([u8; 16]);
9
10impl ObjectId {
11    pub const BOOTSTRAP_NAMESPACE: Self = Self([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
12    pub const BOOTSTRAP_OWNER: Self = Self([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
13
14    /// Allocate a new user identity outside every V6 reserved value.
15    pub fn new() -> Self {
16        loop {
17            let bytes = radixdb_core::new_durable_identity_bytes();
18            if let Ok(id) = Self::from_user_bytes(bytes) {
19                return id;
20            }
21        }
22    }
23
24    /// Decode a persisted non-zero identity, including the two bootstrap IDs.
25    pub fn from_bytes(bytes: [u8; 16]) -> CatalogResult<Self> {
26        if bytes == [0; 16] {
27            return Err(CatalogError::ZeroObjectId);
28        }
29        let id = Self(bytes);
30        if id.is_reserved_unassigned() {
31            return Err(CatalogError::ReservedObjectId {
32                hex: id.to_string(),
33            });
34        }
35        Ok(id)
36    }
37
38    /// Admit an ID allocated for a normal object, excluding bootstrap IDs.
39    pub fn from_user_bytes(bytes: [u8; 16]) -> CatalogResult<Self> {
40        let id = Self::from_bytes(bytes)?;
41        if id.is_bootstrap() {
42            return Err(CatalogError::ReservedObjectId {
43                hex: id.to_string(),
44            });
45        }
46        Ok(id)
47    }
48
49    pub const fn as_bytes(&self) -> &[u8; 16] {
50        &self.0
51    }
52
53    pub const fn into_bytes(self) -> [u8; 16] {
54        self.0
55    }
56
57    pub fn is_bootstrap_namespace(self) -> bool {
58        self == Self::BOOTSTRAP_NAMESPACE
59    }
60
61    pub fn is_bootstrap_owner(self) -> bool {
62        self == Self::BOOTSTRAP_OWNER
63    }
64
65    pub fn is_bootstrap(self) -> bool {
66        self.is_bootstrap_namespace() || self.is_bootstrap_owner()
67    }
68
69    pub fn is_user_allocatable(self) -> bool {
70        !self.has_reserved_prefix()
71    }
72
73    fn has_reserved_prefix(self) -> bool {
74        self.0[..15].iter().all(|byte| *byte == 0)
75    }
76
77    fn is_reserved_unassigned(self) -> bool {
78        self.has_reserved_prefix() && !self.is_bootstrap()
79    }
80}
81
82impl Default for ObjectId {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl fmt::Debug for ObjectId {
89    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90        write!(formatter, "ObjectId({self})")
91    }
92}
93
94impl fmt::Display for ObjectId {
95    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96        for byte in self.0 {
97            write!(formatter, "{byte:02x}")?;
98        }
99        Ok(())
100    }
101}
102
103impl FromStr for ObjectId {
104    type Err = CatalogError;
105
106    fn from_str(value: &str) -> CatalogResult<Self> {
107        if value.len() != 32 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
108            return Err(CatalogError::InvalidObjectIdHex);
109        }
110        let mut bytes = [0_u8; 16];
111        for (index, output) in bytes.iter_mut().enumerate() {
112            *output = u8::from_str_radix(&value[index * 2..index * 2 + 2], 16)
113                .map_err(|_| CatalogError::InvalidObjectIdHex)?;
114        }
115        Self::from_bytes(bytes)
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use std::collections::BTreeSet;
122
123    use super::*;
124
125    #[test]
126    fn exact_bootstrap_values_are_admitted_but_not_user_allocatable() {
127        assert_eq!(
128            ObjectId::from_bytes(ObjectId::BOOTSTRAP_NAMESPACE.into_bytes()).unwrap(),
129            ObjectId::BOOTSTRAP_NAMESPACE
130        );
131        assert_eq!(
132            ObjectId::from_bytes(ObjectId::BOOTSTRAP_OWNER.into_bytes()).unwrap(),
133            ObjectId::BOOTSTRAP_OWNER
134        );
135        assert!(!ObjectId::BOOTSTRAP_NAMESPACE.is_user_allocatable());
136        assert!(!ObjectId::BOOTSTRAP_OWNER.is_user_allocatable());
137    }
138
139    #[test]
140    fn zero_and_unassigned_reserved_range_fail_closed() {
141        assert_eq!(
142            ObjectId::from_bytes([0; 16]),
143            Err(CatalogError::ZeroObjectId)
144        );
145        for value in [3_u8, 19, 255] {
146            let mut bytes = [0_u8; 16];
147            bytes[15] = value;
148            assert!(matches!(
149                ObjectId::from_bytes(bytes),
150                Err(CatalogError::ReservedObjectId { .. })
151            ));
152        }
153        assert!(matches!(
154            ObjectId::from_user_bytes(ObjectId::BOOTSTRAP_NAMESPACE.into_bytes()),
155            Err(CatalogError::ReservedObjectId { .. })
156        ));
157    }
158
159    #[test]
160    fn generated_ids_are_distinct_and_outside_reserved_range() {
161        let ids = (0..1024).map(|_| ObjectId::new()).collect::<BTreeSet<_>>();
162        assert_eq!(ids.len(), 1024);
163        assert!(ids.into_iter().all(ObjectId::is_user_allocatable));
164    }
165
166    #[test]
167    fn raw_byte_order_and_hex_roundtrip_are_canonical() {
168        let id = ObjectId::from_user_bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])
169            .unwrap();
170        assert_eq!(id.to_string(), "0102030405060708090a0b0c0d0e0f10");
171        assert_eq!(id.to_string().parse::<ObjectId>().unwrap(), id);
172        assert_eq!(
173            "0102030405060708090A0B0C0D0E0F10"
174                .parse::<ObjectId>()
175                .unwrap(),
176            id
177        );
178        assert!("01".parse::<ObjectId>().is_err());
179    }
180
181    #[test]
182    fn rename_keeps_identity_while_recreate_allocates_a_new_one() {
183        let original = ObjectId::new();
184        let renamed = original;
185        let recreated = ObjectId::new();
186        assert_eq!(renamed, original);
187        assert_ne!(recreated, original);
188    }
189}