1use crate::impl_storable_bounded;
8use candid::CandidType;
9use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
10use std::{borrow::Borrow, fmt, str::FromStr};
11use thiserror::Error as ThisError;
12
13const TREE_NAME_MAX_BYTES: usize = 40;
14
15#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
22#[serde(transparent)]
23pub struct TreeSpecId(String);
24
25impl TreeSpecId {
26 #[must_use]
27 pub const fn as_str(&self) -> &str {
28 self.0.as_str()
29 }
30}
31
32impl fmt::Display for TreeSpecId {
33 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34 formatter.write_str(self.as_str())
35 }
36}
37
38impl AsRef<str> for TreeSpecId {
39 fn as_ref(&self) -> &str {
40 self.as_str()
41 }
42}
43
44impl Borrow<str> for TreeSpecId {
45 fn borrow(&self) -> &str {
46 self.as_str()
47 }
48}
49
50impl CandidType for TreeSpecId {
51 fn _ty() -> candid::types::Type {
52 candid::types::TypeInner::Text.into()
53 }
54
55 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
56 where
57 S: candid::types::Serializer,
58 {
59 serializer.serialize_text(self.as_str())
60 }
61}
62
63impl FromStr for TreeSpecId {
64 type Err = TreeSpecIdParseError;
65
66 fn from_str(value: &str) -> Result<Self, Self::Err> {
67 validate_tree_name(value).map_err(TreeSpecIdParseError::from)?;
68 Ok(Self(value.to_string()))
69 }
70}
71
72impl TryFrom<String> for TreeSpecId {
73 type Error = TreeSpecIdParseError;
74
75 fn try_from(value: String) -> Result<Self, Self::Error> {
76 validate_tree_name(&value).map_err(TreeSpecIdParseError::from)?;
77 Ok(Self(value))
78 }
79}
80
81impl<'de> Deserialize<'de> for TreeSpecId {
82 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
83 where
84 D: Deserializer<'de>,
85 {
86 let value = String::deserialize(deserializer)?;
87 Self::try_from(value).map_err(de::Error::custom)
88 }
89}
90
91impl_storable_bounded!(TreeSpecId, 64, false);
92
93#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
100#[serde(transparent)]
101pub struct TreeGroupId(String);
102
103impl TreeGroupId {
104 #[must_use]
105 pub const fn as_str(&self) -> &str {
106 self.0.as_str()
107 }
108}
109
110impl fmt::Display for TreeGroupId {
111 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
112 formatter.write_str(self.as_str())
113 }
114}
115
116impl AsRef<str> for TreeGroupId {
117 fn as_ref(&self) -> &str {
118 self.as_str()
119 }
120}
121
122impl Borrow<str> for TreeGroupId {
123 fn borrow(&self) -> &str {
124 self.as_str()
125 }
126}
127
128impl CandidType for TreeGroupId {
129 fn _ty() -> candid::types::Type {
130 candid::types::TypeInner::Text.into()
131 }
132
133 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
134 where
135 S: candid::types::Serializer,
136 {
137 serializer.serialize_text(self.as_str())
138 }
139}
140
141impl FromStr for TreeGroupId {
142 type Err = TreeGroupIdParseError;
143
144 fn from_str(value: &str) -> Result<Self, Self::Err> {
145 validate_tree_name(value).map_err(TreeGroupIdParseError::from)?;
146 Ok(Self(value.to_string()))
147 }
148}
149
150impl TryFrom<String> for TreeGroupId {
151 type Error = TreeGroupIdParseError;
152
153 fn try_from(value: String) -> Result<Self, Self::Error> {
154 validate_tree_name(&value).map_err(TreeGroupIdParseError::from)?;
155 Ok(Self(value))
156 }
157}
158
159impl<'de> Deserialize<'de> for TreeGroupId {
160 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161 where
162 D: Deserializer<'de>,
163 {
164 let value = String::deserialize(deserializer)?;
165 Self::try_from(value).map_err(de::Error::custom)
166 }
167}
168
169impl_storable_bounded!(TreeGroupId, 64, false);
170
171#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
178pub struct TreeId([u8; 32]);
179
180impl TreeId {
181 #[must_use]
186 pub const fn from_generated_bytes(bytes: [u8; 32]) -> Self {
187 Self(bytes)
188 }
189
190 #[must_use]
191 pub const fn as_bytes(&self) -> &[u8; 32] {
192 &self.0
193 }
194}
195
196impl fmt::Display for TreeId {
197 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
198 for byte in self.0 {
199 write!(formatter, "{byte:02x}")?;
200 }
201 Ok(())
202 }
203}
204
205impl CandidType for TreeId {
206 fn _ty() -> candid::types::Type {
207 candid::types::TypeInner::Text.into()
208 }
209
210 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
211 where
212 S: candid::types::Serializer,
213 {
214 serializer.serialize_text(&self.to_string())
215 }
216}
217
218impl FromStr for TreeId {
219 type Err = TreeIdParseError;
220
221 fn from_str(value: &str) -> Result<Self, Self::Err> {
222 if value.len() != 64 {
223 return Err(TreeIdParseError::Length(value.len()));
224 }
225 if !value
226 .bytes()
227 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
228 {
229 return Err(TreeIdParseError::CanonicalHex);
230 }
231
232 let mut bytes = [0; 32];
233 for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
234 bytes[index] = (decode_nibble(pair[0]) << 4) | decode_nibble(pair[1]);
235 }
236 Ok(Self(bytes))
237 }
238}
239
240impl Serialize for TreeId {
241 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
242 where
243 S: Serializer,
244 {
245 serializer.collect_str(self)
246 }
247}
248
249impl<'de> Deserialize<'de> for TreeId {
250 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
251 where
252 D: Deserializer<'de>,
253 {
254 let value = String::deserialize(deserializer)?;
255 value.parse().map_err(de::Error::custom)
256 }
257}
258
259#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
266pub enum TreeSpecIdParseError {
267 #[error("Tree Spec ID must not be empty")]
268 Empty,
269
270 #[error("Tree Spec ID must not exceed {max_bytes} bytes, got {actual_bytes}")]
271 TooLong {
272 max_bytes: usize,
273 actual_bytes: usize,
274 },
275
276 #[error("Tree Spec ID must use only ASCII letters, numbers, '-' or '_'")]
277 InvalidCharacters,
278}
279
280#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
287pub enum TreeGroupIdParseError {
288 #[error("Tree Group ID must not be empty")]
289 Empty,
290
291 #[error("Tree Group ID must not exceed {max_bytes} bytes, got {actual_bytes}")]
292 TooLong {
293 max_bytes: usize,
294 actual_bytes: usize,
295 },
296
297 #[error("Tree Group ID must use only ASCII letters, numbers, '-' or '_'")]
298 InvalidCharacters,
299}
300
301#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
308pub enum TreeIdParseError {
309 #[error("Tree ID must contain exactly 64 characters, got {0}")]
310 Length(usize),
311
312 #[error("Tree ID must contain only lowercase hexadecimal characters")]
313 CanonicalHex,
314}
315
316#[derive(Clone, Copy)]
317enum TreeNameIssue {
318 Empty,
319 TooLong {
320 max_bytes: usize,
321 actual_bytes: usize,
322 },
323 InvalidCharacters,
324}
325
326impl From<TreeNameIssue> for TreeSpecIdParseError {
327 fn from(issue: TreeNameIssue) -> Self {
328 match issue {
329 TreeNameIssue::Empty => Self::Empty,
330 TreeNameIssue::TooLong {
331 max_bytes,
332 actual_bytes,
333 } => Self::TooLong {
334 max_bytes,
335 actual_bytes,
336 },
337 TreeNameIssue::InvalidCharacters => Self::InvalidCharacters,
338 }
339 }
340}
341
342impl From<TreeNameIssue> for TreeGroupIdParseError {
343 fn from(issue: TreeNameIssue) -> Self {
344 match issue {
345 TreeNameIssue::Empty => Self::Empty,
346 TreeNameIssue::TooLong {
347 max_bytes,
348 actual_bytes,
349 } => Self::TooLong {
350 max_bytes,
351 actual_bytes,
352 },
353 TreeNameIssue::InvalidCharacters => Self::InvalidCharacters,
354 }
355 }
356}
357
358fn validate_tree_name(value: &str) -> Result<(), TreeNameIssue> {
359 if value.is_empty() {
360 return Err(TreeNameIssue::Empty);
361 }
362 if value.len() > TREE_NAME_MAX_BYTES {
363 return Err(TreeNameIssue::TooLong {
364 max_bytes: TREE_NAME_MAX_BYTES,
365 actual_bytes: value.len(),
366 });
367 }
368 if !value
369 .bytes()
370 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
371 {
372 return Err(TreeNameIssue::InvalidCharacters);
373 }
374 Ok(())
375}
376
377fn decode_nibble(byte: u8) -> u8 {
378 match byte {
379 b'0'..=b'9' => byte - b'0',
380 b'a'..=b'f' => byte - b'a' + 10,
381 _ => unreachable!("canonical hex was validated before decoding"),
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use crate::cdk::structures::storable::Storable;
388
389 use super::*;
390
391 #[test]
392 fn declared_tree_ids_are_bounded_canonical_names() {
393 let spec = "users".parse::<TreeSpecId>().expect("Tree Spec ID");
394 let group = "users-primary"
395 .parse::<TreeGroupId>()
396 .expect("Tree Group ID");
397
398 assert_eq!(spec.as_str(), "users");
399 assert_eq!(group.as_str(), "users-primary");
400 std::assert_matches!("".parse::<TreeSpecId>(), Err(TreeSpecIdParseError::Empty));
401 std::assert_matches!(
402 "bad/name".parse::<TreeGroupId>(),
403 Err(TreeGroupIdParseError::InvalidCharacters)
404 );
405 std::assert_matches!(
406 "a".repeat(TREE_NAME_MAX_BYTES + 1).parse::<TreeSpecId>(),
407 Err(TreeSpecIdParseError::TooLong { .. })
408 );
409 }
410
411 #[test]
412 fn declared_tree_ids_validate_serde_and_candid_input() {
413 let mut invalid_bytes = Vec::new();
414 ciborium::ser::into_writer("bad/name", &mut invalid_bytes)
415 .expect("serialize invalid Tree Spec ID");
416
417 assert!(ciborium::de::from_reader::<TreeSpecId, _>(invalid_bytes.as_slice()).is_err());
418
419 let candid_bytes = candid::encode_one("projects").expect("encode Tree Group ID");
420 let group = candid::decode_one::<TreeGroupId>(&candid_bytes).expect("decode Tree Group ID");
421 let invalid_candid = candid::encode_one("bad/name").expect("encode invalid Tree Group ID");
422
423 assert_eq!(group.as_str(), "projects");
424 assert!(candid::decode_one::<TreeGroupId>(&invalid_candid).is_err());
425 }
426
427 #[test]
428 fn declared_tree_ids_fit_their_stable_bound() {
429 let spec = "a"
430 .repeat(TREE_NAME_MAX_BYTES)
431 .parse::<TreeSpecId>()
432 .expect("maximum Tree Spec ID");
433 let group = "b"
434 .repeat(TREE_NAME_MAX_BYTES)
435 .parse::<TreeGroupId>()
436 .expect("maximum Tree Group ID");
437 let spec_bytes = spec.to_bytes();
438 let group_bytes = group.to_bytes();
439
440 assert!(spec_bytes.len() <= 64);
441 assert!(group_bytes.len() <= 64);
442 assert_eq!(TreeSpecId::from_bytes(spec_bytes), spec);
443 assert_eq!(TreeGroupId::from_bytes(group_bytes), group);
444 }
445
446 #[test]
447 fn tree_id_uses_exact_canonical_text() {
448 let tree_id = TreeId::from_generated_bytes([0xab; 32]);
449 let text = "ab".repeat(32);
450
451 assert_eq!(tree_id.to_string(), text);
452 assert_eq!(text.parse::<TreeId>(), Ok(tree_id));
453
454 let mut serde_bytes = Vec::new();
455 ciborium::ser::into_writer(&tree_id, &mut serde_bytes).expect("serialize Tree ID");
456 let decoded: TreeId =
457 ciborium::de::from_reader(serde_bytes.as_slice()).expect("decode Tree ID");
458 assert_eq!(decoded, tree_id);
459
460 let candid_bytes = candid::encode_one(tree_id).expect("encode Tree ID");
461 let decoded: TreeId = candid::decode_one(&candid_bytes).expect("decode Tree ID");
462 let invalid_candid =
463 candid::encode_one("A000000000000000000000000000000000000000000000000000000000000000")
464 .expect("encode invalid Tree ID");
465
466 assert_eq!(decoded, tree_id);
467 assert!(candid::decode_one::<TreeId>(&invalid_candid).is_err());
468 }
469
470 #[test]
471 fn tree_id_rejects_noncanonical_text() {
472 std::assert_matches!("ab".parse::<TreeId>(), Err(TreeIdParseError::Length(2)));
473 std::assert_matches!(
474 "A000000000000000000000000000000000000000000000000000000000000000".parse::<TreeId>(),
475 Err(TreeIdParseError::CanonicalHex)
476 );
477 }
478}