icydb_core/db/identity/
mod.rs1#![expect(clippy::cast_possible_truncation)]
2#[cfg(test)]
12mod tests;
13
14use crate::MAX_INDEX_FIELDS;
15use std::{
16 cmp::Ordering,
17 fmt::{self, Display},
18};
19use thiserror::Error as ThisError;
20
21pub(super) const MAX_ENTITY_NAME_LEN: usize = 64;
26pub(super) const MAX_INDEX_FIELD_NAME_LEN: usize = 64;
27pub(super) const MAX_INDEX_NAME_LEN: usize =
28 MAX_ENTITY_NAME_LEN + (MAX_INDEX_FIELDS * (MAX_INDEX_FIELD_NAME_LEN + 1));
29
30#[derive(Debug, ThisError)]
36pub enum IdentityDecodeError {
37 #[error("invalid size")]
38 InvalidSize,
39
40 #[error("invalid length")]
41 InvalidLength,
42
43 #[error("non-ascii encoding")]
44 NonAscii,
45
46 #[error("non-zero padding")]
47 NonZeroPadding,
48}
49
50#[derive(Debug, ThisError)]
55pub enum EntityNameError {
56 #[error("entity name is empty")]
57 Empty,
58
59 #[error("entity name length {len} exceeds max {max}")]
60 TooLong { len: usize, max: usize },
61
62 #[error("entity name must be ASCII")]
63 NonAscii,
64}
65
66#[derive(Debug, ThisError)]
71pub enum IndexNameError {
72 #[error("index has {len} fields (max {max})")]
73 TooManyFields { len: usize, max: usize },
74
75 #[error("index field name '{field}' exceeds max length {max}")]
76 FieldTooLong { field: String, max: usize },
77
78 #[error("index field name '{field}' must be ASCII")]
79 FieldNonAscii { field: String },
80
81 #[error("index name length {len} exceeds max {max}")]
82 TooLong { len: usize, max: usize },
83}
84
85#[derive(Clone, Copy, Eq, Hash, PartialEq)]
90pub struct EntityName {
91 len: u8,
92 bytes: [u8; MAX_ENTITY_NAME_LEN],
93}
94
95impl EntityName {
96 pub const STORED_SIZE_BYTES: u64 = 1 + (MAX_ENTITY_NAME_LEN as u64);
98
99 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
101
102 pub fn try_from_str(name: &str) -> Result<Self, EntityNameError> {
103 let bytes = name.as_bytes();
104 let len = bytes.len();
105
106 if len == 0 {
107 return Err(EntityNameError::Empty);
108 }
109 if len > MAX_ENTITY_NAME_LEN {
110 return Err(EntityNameError::TooLong {
111 len,
112 max: MAX_ENTITY_NAME_LEN,
113 });
114 }
115 if !bytes.is_ascii() {
116 return Err(EntityNameError::NonAscii);
117 }
118
119 let mut out = [0u8; MAX_ENTITY_NAME_LEN];
120 out[..len].copy_from_slice(bytes);
121
122 Ok(Self {
123 len: len as u8,
124 bytes: out,
125 })
126 }
127
128 #[must_use]
129 pub const fn len(&self) -> usize {
130 self.len as usize
131 }
132
133 #[must_use]
134 pub const fn is_empty(&self) -> bool {
135 self.len() == 0
136 }
137
138 #[must_use]
139 pub fn as_bytes(&self) -> &[u8] {
140 &self.bytes[..self.len()]
141 }
142
143 #[must_use]
144 pub fn as_str(&self) -> &str {
145 std::str::from_utf8(self.as_bytes()).expect("EntityName invariant: ASCII-only storage")
148 }
149
150 #[must_use]
151 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
152 let mut out = [0u8; Self::STORED_SIZE_USIZE];
153 out[0] = self.len;
154 out[1..].copy_from_slice(&self.bytes);
155 out
156 }
157
158 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
159 if bytes.len() != Self::STORED_SIZE_USIZE {
160 return Err(IdentityDecodeError::InvalidSize);
161 }
162
163 let len = bytes[0] as usize;
164 if len == 0 || len > MAX_ENTITY_NAME_LEN {
165 return Err(IdentityDecodeError::InvalidLength);
166 }
167 if !bytes[1..=len].is_ascii() {
168 return Err(IdentityDecodeError::NonAscii);
169 }
170 if bytes[1 + len..].iter().any(|&b| b != 0) {
171 return Err(IdentityDecodeError::NonZeroPadding);
172 }
173
174 let mut name = [0u8; MAX_ENTITY_NAME_LEN];
175 name.copy_from_slice(&bytes[1..]);
176
177 Ok(Self {
178 len: len as u8,
179 bytes: name,
180 })
181 }
182
183 #[must_use]
184 pub const fn max_storable() -> Self {
185 Self {
186 len: MAX_ENTITY_NAME_LEN as u8,
187 bytes: [b'z'; MAX_ENTITY_NAME_LEN],
188 }
189 }
190}
191
192impl Ord for EntityName {
193 fn cmp(&self, other: &Self) -> Ordering {
194 self.len.cmp(&other.len).then(self.bytes.cmp(&other.bytes))
197 }
198}
199
200impl PartialOrd for EntityName {
201 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
202 Some(self.cmp(other))
203 }
204}
205
206impl Display for EntityName {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 f.write_str(self.as_str())
209 }
210}
211
212impl fmt::Debug for EntityName {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 write!(f, "EntityName({})", self.as_str())
215 }
216}
217
218#[derive(Clone, Copy, Eq, Hash, PartialEq)]
223pub struct IndexName {
224 len: u16,
225 bytes: [u8; MAX_INDEX_NAME_LEN],
226}
227
228impl IndexName {
229 pub const STORED_SIZE_BYTES: u64 = 2 + (MAX_INDEX_NAME_LEN as u64);
230 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
231
232 pub fn try_from_parts(entity: &EntityName, fields: &[&str]) -> Result<Self, IndexNameError> {
233 if fields.len() > MAX_INDEX_FIELDS {
234 return Err(IndexNameError::TooManyFields {
235 len: fields.len(),
236 max: MAX_INDEX_FIELDS,
237 });
238 }
239
240 let mut total_len = entity.len();
241 for field in fields {
242 let field_len = field.len();
243 if field_len > MAX_INDEX_FIELD_NAME_LEN {
244 return Err(IndexNameError::FieldTooLong {
245 field: (*field).to_string(),
246 max: MAX_INDEX_FIELD_NAME_LEN,
247 });
248 }
249 if !field.is_ascii() {
250 return Err(IndexNameError::FieldNonAscii {
251 field: (*field).to_string(),
252 });
253 }
254 total_len = total_len.saturating_add(1 + field_len);
255 }
256
257 if total_len > MAX_INDEX_NAME_LEN {
258 return Err(IndexNameError::TooLong {
259 len: total_len,
260 max: MAX_INDEX_NAME_LEN,
261 });
262 }
263
264 let mut out = [0u8; MAX_INDEX_NAME_LEN];
265 let mut len = 0usize;
266
267 Self::push_bytes(&mut out, &mut len, entity.as_bytes());
268 for field in fields {
269 Self::push_bytes(&mut out, &mut len, b"|");
270 Self::push_bytes(&mut out, &mut len, field.as_bytes());
271 }
272
273 Ok(Self {
274 len: len as u16,
275 bytes: out,
276 })
277 }
278
279 #[must_use]
280 pub fn as_bytes(&self) -> &[u8] {
281 &self.bytes[..self.len as usize]
282 }
283
284 #[must_use]
285 pub fn as_str(&self) -> &str {
286 std::str::from_utf8(self.as_bytes()).expect("IndexName invariant: ASCII-only storage")
289 }
290
291 #[must_use]
292 pub fn to_bytes(self) -> [u8; Self::STORED_SIZE_USIZE] {
293 let mut out = [0u8; Self::STORED_SIZE_USIZE];
294 out[..2].copy_from_slice(&self.len.to_be_bytes());
295 out[2..].copy_from_slice(&self.bytes);
296 out
297 }
298
299 pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityDecodeError> {
300 if bytes.len() != Self::STORED_SIZE_USIZE {
301 return Err(IdentityDecodeError::InvalidSize);
302 }
303
304 let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
305 if len == 0 || len > MAX_INDEX_NAME_LEN {
306 return Err(IdentityDecodeError::InvalidLength);
307 }
308 if !bytes[2..2 + len].is_ascii() {
309 return Err(IdentityDecodeError::NonAscii);
310 }
311 if bytes[2 + len..].iter().any(|&b| b != 0) {
312 return Err(IdentityDecodeError::NonZeroPadding);
313 }
314
315 let mut name = [0u8; MAX_INDEX_NAME_LEN];
316 name.copy_from_slice(&bytes[2..]);
317
318 Ok(Self {
319 len: len as u16,
320 bytes: name,
321 })
322 }
323
324 fn push_bytes(out: &mut [u8; MAX_INDEX_NAME_LEN], len: &mut usize, bytes: &[u8]) {
325 let end = *len + bytes.len();
326 out[*len..end].copy_from_slice(bytes);
327 *len = end;
328 }
329
330 #[must_use]
331 pub const fn max_storable() -> Self {
332 Self {
333 len: MAX_INDEX_NAME_LEN as u16,
334 bytes: [b'z'; MAX_INDEX_NAME_LEN],
335 }
336 }
337}
338
339impl Ord for IndexName {
340 fn cmp(&self, other: &Self) -> Ordering {
341 self.to_bytes().cmp(&other.to_bytes())
342 }
343}
344
345impl PartialOrd for IndexName {
346 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
347 Some(self.cmp(other))
348 }
349}
350
351impl fmt::Debug for IndexName {
352 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353 write!(f, "IndexName({})", self.as_str())
354 }
355}
356
357impl Display for IndexName {
358 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359 f.write_str(self.as_str())
360 }
361}