radixdb_catalog/
identity.rs1use std::fmt;
2use std::str::FromStr;
3
4use crate::{CatalogError, CatalogResult};
5
6#[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 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 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 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}