1use crate::hex::{hex_encode_bytes, is_lower_hex_byte};
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use thiserror::Error;
9
10const SERVER_GENERATED_ID_BODY_LEN: usize = 32;
11
12macro_rules! validation_error {
17 ($name:ident, $message:literal) => {
18 #[derive(Debug, Clone, PartialEq, Eq, Error)]
20 #[error($message)]
21 pub struct $name {
22 value: String,
23 reason: String,
24 }
25
26 impl $name {
27 pub(crate) fn new(value: &str, reason: impl Into<String>) -> Self {
28 Self {
29 value: value.to_owned(),
30 reason: reason.into(),
31 }
32 }
33
34 pub fn value(&self) -> &str {
36 &self.value
37 }
38
39 pub fn reason(&self) -> &str {
41 &self.reason
42 }
43 }
44 };
45}
46
47validation_error!(
48 NamespaceIdValidationError,
49 "invalid namespace_id {value:?}: {reason}"
50);
51validation_error!(
52 CommitIdValidationError,
53 "invalid commit_id {value:?}: {reason}"
54);
55validation_error!(
56 GeneratedIdValidationError,
57 "invalid generated id {value:?}: {reason}"
58);
59validation_error!(
60 SnapshotIdValidationError,
61 "invalid snapshot_id {value:?}: {reason}"
62);
63validation_error!(
64 NameKeyValidationError,
65 "invalid name_key {value:?}: {reason}"
66);
67validation_error!(
68 WriterIdValidationError,
69 "invalid writer_id {value:?}: {reason}"
70);
71validation_error!(
72 BindingGenerationValidationError,
73 "invalid binding_generation {value:?}: {reason}"
74);
75
76macro_rules! string_id {
103 (
104 $(#[$meta:meta])*
105 $name:ident,
106 error = $error:ty,
107 validate = $validate:expr
108 $(, schema($($schema:tt)+))?
109 $(,)?
110 ) => {
111 $(#[$meta])*
112 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
113 #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
114 #[cfg_attr(
115 feature = "openapi",
116 schema(value_type = String $(, $($schema)+)?)
117 )]
118 pub struct $name(String);
119
120 impl $name {
121 pub fn parse(value: impl AsRef<str>) -> Result<Self, $error> {
123 let value = value.as_ref();
124 ($validate)(value)?;
125 Ok(Self(value.to_owned()))
126 }
127
128 pub fn as_str(&self) -> &str {
130 &self.0
131 }
132 }
133
134 impl TryFrom<&str> for $name {
135 type Error = $error;
136
137 fn try_from(value: &str) -> Result<Self, Self::Error> {
138 Self::parse(value)
139 }
140 }
141
142 impl TryFrom<String> for $name {
143 type Error = $error;
144
145 fn try_from(value: String) -> Result<Self, Self::Error> {
146 Self::parse(value)
147 }
148 }
149
150 impl std::str::FromStr for $name {
151 type Err = $error;
152
153 fn from_str(value: &str) -> Result<Self, Self::Err> {
154 Self::parse(value)
155 }
156 }
157
158 impl AsRef<str> for $name {
159 fn as_ref(&self) -> &str {
160 self.as_str()
161 }
162 }
163
164 impl std::borrow::Borrow<str> for $name {
165 fn borrow(&self) -> &str {
166 self.as_str()
167 }
168 }
169
170 impl std::fmt::Display for $name {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 f.write_str(&self.0)
173 }
174 }
175
176 impl serde::Serialize for $name {
177 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
178 where
179 S: serde::Serializer,
180 {
181 serializer.serialize_str(&self.0)
182 }
183 }
184
185 impl<'de> serde::Deserialize<'de> for $name {
186 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
187 where
188 D: serde::Deserializer<'de>,
189 {
190 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
191 Self::parse(value).map_err(serde::de::Error::custom)
192 }
193 }
194 };
195 (
196 $(#[$meta:meta])*
197 $name:ident,
198 prefix = $prefix:literal
199 $(, schema($($schema:tt)+))?
200 $(,)?
201 ) => {
202 string_id! {
203 $(#[$meta])*
204 $name,
205 error = GeneratedIdValidationError,
206 validate = |value: &str| validate_generated_id($prefix, value)
207 $(, schema($($schema)+))?
208 }
209
210 impl $name {
211 pub fn generate() -> Self {
213 Self(generated_id($prefix))
214 }
215 }
216 };
217}
218
219macro_rules! numeric_id {
226 (
227 $(#[$meta:meta])*
228 $name:ident,
229 public_ordinal,
230 schema_description = $schema_description:literal
231 ) => {
232 $(#[$meta])*
233 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
234 pub struct $name(pub u64);
235
236 impl $name {
237 pub fn parse(value: u64) -> Result<Self, $crate::PublicOrdinalRangeError> {
244 if value > $crate::MAX_PUBLIC_INTEGER {
245 return Err($crate::PublicOrdinalRangeError);
246 }
247 Ok(Self(value))
248 }
249
250 pub fn successor(self) -> Result<Self, $crate::PublicOrdinalRangeError> {
252 $crate::next_public_ordinal(self.0)
253 .map(Self)
254 .ok_or($crate::PublicOrdinalRangeError)
255 }
256 }
257
258 #[cfg(feature = "openapi")]
259 impl utoipa::PartialSchema for $name {
260 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
261 utoipa::openapi::schema::Object::builder()
262 .schema_type(utoipa::openapi::schema::Type::Integer)
263 .format(Some(utoipa::openapi::SchemaFormat::KnownFormat(
264 utoipa::openapi::KnownFormat::Int64,
265 )))
266 .minimum(Some(0u64))
267 .maximum(Some($crate::MAX_PUBLIC_INTEGER))
268 .description(Some($schema_description))
269 .into()
270 }
271 }
272
273 #[cfg(feature = "openapi")]
274 impl utoipa::ToSchema for $name {}
275
276 impl<'de> serde::Deserialize<'de> for $name {
277 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
278 where
279 D: serde::Deserializer<'de>,
280 {
281 let value = <u64 as serde::Deserialize>::deserialize(deserializer)?;
282 Self::parse(value).map_err(serde::de::Error::custom)
283 }
284 }
285
286 impl From<u64> for $name {
287 fn from(value: u64) -> Self {
288 Self(value)
289 }
290 }
291
292 impl fmt::Display for $name {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 write!(f, "{}", self.0)
295 }
296 }
297 };
298 (
299 $(#[$meta:meta])*
300 $name:ident
301 ) => {
302 $(#[$meta])*
303 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
304 pub struct $name(pub u64);
305
306 impl From<u64> for $name {
307 fn from(value: u64) -> Self {
308 Self(value)
309 }
310 }
311
312 impl fmt::Display for $name {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314 write!(f, "{}", self.0)
315 }
316 }
317 };
318}
319
320pub(crate) use numeric_id;
321pub(crate) use string_id;
322pub(crate) use validation_error;
323
324pub fn generated_id(prefix: &'static str) -> String {
337 format!("{prefix}_{}", hex_encode_bytes(&random_128()))
338}
339
340fn random_128() -> [u8; 16] {
345 let mut bytes = [0_u8; 16];
346 getrandom::fill(&mut bytes).expect("the system random generator must be available");
347 bytes
348}
349
350fn validate_generated_id(
351 prefix: &'static str,
352 value: &str,
353) -> Result<(), GeneratedIdValidationError> {
354 let expected_prefix = format!("{prefix}_");
355 let Some(body) = value.strip_prefix(&expected_prefix) else {
356 return Err(GeneratedIdValidationError::new(
357 value,
358 format!("must start with `{expected_prefix}`"),
359 ));
360 };
361 if body.len() != SERVER_GENERATED_ID_BODY_LEN {
362 return Err(GeneratedIdValidationError::new(
363 value,
364 format!("body must be {SERVER_GENERATED_ID_BODY_LEN} lowercase hex characters"),
365 ));
366 }
367 if !body.bytes().all(is_lower_hex_byte) {
368 return Err(GeneratedIdValidationError::new(
369 value,
370 "body must contain only lowercase hex characters".to_owned(),
371 ));
372 }
373 Ok(())
374}
375
376fn validate_namespace_id(value: &str) -> Result<(), NamespaceIdValidationError> {
377 validate_id_grammar(value).map_err(|reason| NamespaceIdValidationError::new(value, reason))?;
378 if value.starts_with("loonfs-") {
381 return Err(NamespaceIdValidationError::new(
382 value,
383 "the `loonfs-` prefix is reserved for LoonFS system namespaces",
384 ));
385 }
386 Ok(())
387}
388
389fn validate_commit_id(value: &str) -> Result<(), CommitIdValidationError> {
390 validate_id_grammar(value).map_err(|reason| CommitIdValidationError::new(value, reason))
391}
392
393pub const MAX_NAME_KEY_BYTES: usize = 768;
398pub const MAX_ID_BYTES: usize = 128;
400
401fn validate_name_key(value: &str) -> Result<(), NameKeyValidationError> {
402 if value.is_empty() {
403 return Err(NameKeyValidationError::new(value, "must not be empty"));
404 }
405 if value.contains('/') {
406 return Err(NameKeyValidationError::new(value, "must not contain `/`"));
407 }
408 if matches!(value, "." | "..") {
409 return Err(NameKeyValidationError::new(
410 value,
411 "must not be `.` or `..`",
412 ));
413 }
414 if value.chars().any(|character| character.is_control()) {
415 return Err(NameKeyValidationError::new(
416 value,
417 "must not contain control characters",
418 ));
419 }
420 if value.len() > MAX_NAME_KEY_BYTES {
421 return Err(NameKeyValidationError::new(
423 "",
424 format!("exceeds the maximum name key length of {MAX_NAME_KEY_BYTES} bytes"),
425 ));
426 }
427 Ok(())
428}
429
430fn validate_id_grammar(value: &str) -> Result<(), String> {
431 if value.is_empty() {
432 return Err("must not be empty".to_owned());
433 }
434 if value.len() > MAX_ID_BYTES {
435 return Err(format!("must be {MAX_ID_BYTES} bytes or fewer"));
436 }
437 if value.trim() != value {
438 return Err("must not have leading or trailing whitespace".to_owned());
439 }
440 if matches!(value, "." | "..") {
441 return Err("must not be `.` or `..`".to_owned());
442 }
443
444 let mut chars = value.chars();
445 let first = chars
446 .next()
447 .expect("empty id returned before char validation");
448 if !first.is_ascii_lowercase() && !first.is_ascii_digit() {
449 return Err("must start with a lowercase ASCII letter or digit".to_owned());
450 }
451 if !chars.all(is_allowed_id_tail_char) {
452 return Err(
453 "must contain only lowercase ASCII letters, digits, `.`, `_`, or `-`".to_owned(),
454 );
455 }
456
457 Ok(())
458}
459
460fn is_allowed_id_tail_char(ch: char) -> bool {
461 ch.is_ascii_lowercase() || ch.is_ascii_digit() || matches!(ch, '.' | '_' | '-')
462}
463
464string_id! {
469 NamespaceId,
476 error = NamespaceIdValidationError,
477 validate = validate_namespace_id,
478 schema(
479 pattern = r"^[a-z0-9][a-z0-9._-]{0,127}$",
483 example = "demo"
484 )
485}
486
487string_id! {
488 WriterId,
490 error = WriterIdValidationError,
491 validate = |value: &str| {
492 if value.trim().is_empty() {
493 return Err(WriterIdValidationError::new(value, "must not be blank"));
494 }
495 Ok(())
496 }
497}
498
499string_id! {
500 BindingGeneration,
502 error = BindingGenerationValidationError,
503 validate = |value: &str| {
504 if value.is_empty() {
505 return Err(BindingGenerationValidationError::new(value, "must not be empty"));
506 }
507 if !value.bytes().all(is_lower_hex_byte) {
508 return Err(BindingGenerationValidationError::new(
509 value,
510 "must contain only lowercase hex characters",
511 ));
512 }
513 Ok(())
514 },
515 schema(pattern = r"^[0-9a-f]+$")
516}
517
518string_id! {
519 ContentStoreId,
523 prefix = "cs"
524}
525
526string_id! {
527 CommitId,
534 error = CommitIdValidationError,
535 validate = validate_commit_id,
536 schema(
537 pattern = r"^[a-z0-9][a-z0-9._-]{0,127}$",
538 example = "c_f3a9c2d4b6e8417a90c5d2f8e1b7a6c0"
539 )
540}
541
542impl CommitId {
543 pub fn generate() -> Self {
545 Self(generated_id("c"))
546 }
547}
548
549string_id! {
550 CheckpointId,
554 error = GeneratedIdValidationError,
555 validate = validate_checkpoint_id,
556 schema(
557 pattern = r"^pin_[0-9]{20}-[0-9a-f]{16}$",
558 example = "pin_00000000000000000001-0000000000000002"
559 )
560}
561
562string_id! {
563 SnapshotId,
568 error = SnapshotIdValidationError,
569 validate = |value: &str| validate_checkpoint_id(value)
570 .map_err(|error| SnapshotIdValidationError::new(&error.value, error.reason)),
571 schema(
572 pattern = r"^pin_[0-9]{20}-[0-9a-f]{16}$",
573 example = "pin_00000000000000000001-0000000000000002"
574 )
575}
576
577impl From<SnapshotId> for CheckpointId {
578 fn from(snapshot_id: SnapshotId) -> Self {
579 Self(snapshot_id.0)
580 }
581}
582
583impl From<CheckpointId> for SnapshotId {
584 fn from(checkpoint_id: CheckpointId) -> Self {
585 Self(checkpoint_id.0)
586 }
587}
588
589impl CheckpointId {
590 pub fn generate(manifest_no: ManifestNo) -> Self {
592 let entropy = generated_id("pin");
593 Self::parse(format!("pin_{:020}-{}", manifest_no.0, &entropy[4..20]))
594 .expect("the pinned manifest number should be valid")
595 }
596
597 pub fn manifest_no(&self) -> ManifestNo {
599 ManifestNo(
600 self.0[4..24]
601 .parse()
602 .expect("a pin id should contain a manifest number"),
603 )
604 }
605}
606
607fn validate_checkpoint_id(value: &str) -> Result<(), GeneratedIdValidationError> {
608 let valid = value
609 .strip_prefix("pin_")
610 .and_then(|body| body.split_once('-'))
611 .is_some_and(|(number, entropy)| {
612 number.len() == 20
613 && number.bytes().all(|byte| byte.is_ascii_digit())
614 && number
615 .parse::<u64>()
616 .ok()
617 .and_then(|number| ManifestNo::parse(number).ok())
618 .is_some_and(|number| number.0 > 0)
619 && entropy.len() == 16
620 && entropy.bytes().all(is_lower_hex_byte)
621 });
622 if !valid {
623 return Err(GeneratedIdValidationError::new(value, "must be `pin_` followed by a twenty-digit positive manifest number, `-`, and sixteen lowercase hex characters".to_owned()));
624 }
625 Ok(())
626}
627
628string_id! {
629 UploadId,
631 prefix = "upl",
632 schema(
633 pattern = r"^upl_[0-9a-f]{32}$",
634 example = "upl_4d8f2c91a7b34e0f9c6d1a2b3e5f708c"
635 )
636}
637
638string_id! {
639 ContentId,
647 prefix = "con",
648 schema(
649 pattern = r"^con_[0-9a-f]{32}$",
650 example = "con_9f2a6c0e4b7d4a90b13f0d8c5e6a2b41"
651 )
652}
653
654impl ContentId {
655 pub fn shard_prefixes(&self) -> [&str; CONTENT_ID_SHARD_LEVELS] {
660 let first_start = CONTENT_ID_PREFIX_LEN;
661 let second_start = first_start + CONTENT_ID_SHARD_WIDTH;
662 [
663 &self.0[first_start..second_start],
664 &self.0[second_start..second_start + CONTENT_ID_SHARD_WIDTH],
665 ]
666 }
667}
668
669const CONTENT_ID_PREFIX_LEN: usize = "con_".len();
671const CONTENT_ID_SHARD_LEVELS: usize = 2;
673const CONTENT_ID_SHARD_WIDTH: usize = 2;
675
676string_id! {
677 MetadataSegmentId,
679 prefix = "seg"
680}
681
682string_id! {
683 MetadataCompactionId,
685 prefix = "cmp"
686}
687
688string_id! {
689 IndexSegmentId,
691 prefix = "idx"
692}
693
694string_id! {
695 NameKey,
700 error = NameKeyValidationError,
701 validate = validate_name_key,
702 schema(example = "report.txt")
703}
704
705impl NameKey {
706 pub fn for_display_name(display_name: &crate::DisplayName) -> Self {
708 Self(crate::name_key_for_display_name(display_name.as_str()))
709 }
710}
711
712pub const MAX_PUBLIC_INTEGER: u64 = 9_007_199_254_740_991;
721
722#[derive(Debug, Clone, Copy, PartialEq, Eq)]
724pub struct PublicOrdinalRangeError;
725
726impl fmt::Display for PublicOrdinalRangeError {
727 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
728 write!(f, "must be an integer from 0 through {MAX_PUBLIC_INTEGER}")
729 }
730}
731
732impl std::error::Error for PublicOrdinalRangeError {}
733
734pub fn next_public_ordinal(current: u64) -> Option<u64> {
736 current
737 .checked_add(1)
738 .filter(|next| *next <= MAX_PUBLIC_INTEGER)
739}
740
741numeric_id! {
742 InodeId
746}
747
748#[cfg(feature = "openapi")]
749#[allow(
750 deprecated,
751 reason = "the published schema uses the requested singular example field"
752)]
753impl utoipa::PartialSchema for InodeId {
754 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
755 utoipa::openapi::schema::Object::builder()
756 .schema_type(utoipa::openapi::schema::Type::String)
757 .pattern(Some(crate::public_inode_id::PATTERN))
758 .example(Some(serde_json::json!(crate::public_inode_id::EXAMPLE)))
759 .description(Some(crate::public_inode_id::DESCRIPTION))
760 .into()
761 }
762}
763
764#[cfg(feature = "openapi")]
765impl utoipa::ToSchema for InodeId {}
766
767pub const ROOT_INODE_ID: InodeId = InodeId(1);
769
770pub const FIRST_ALLOCATABLE_INODE_ID: InodeId = InodeId(ROOT_INODE_ID.0 + 1);
772
773numeric_id! {
774 RevisionNo,
776 public_ordinal,
777 schema_description = "Revision number for a file's content. It increases whenever the content is replaced or restored."
778}
779
780numeric_id! {
781 ChangeSeq,
785 public_ordinal,
786 schema_description = "Sequence number assigned to a namespace commit. It determines the order in which commits become visible."
787}
788
789numeric_id! {
790 WalNo,
792 public_ordinal,
793 schema_description = "Contiguous WAL object number within one namespace."
794}
795
796numeric_id! {
797 ManifestNo,
802 public_ordinal,
803 schema_description = "Monotonic manifest counter for one namespace. It can increase when metadata changes, even if no namespace commit is written."
804}
805
806numeric_id! {
807 RunNo,
813 public_ordinal,
814 schema_description = "Monotonic run counter allocated by the manifest that names the run. A run is the set of segments one producer wrote together."
815}
816
817numeric_id! {
818 WriterEpoch,
820 public_ordinal,
821 schema_description = "Counter used to reject writes from an older writer."
822}
823
824#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
830#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
831#[serde(rename_all = "snake_case")]
832pub enum InodeKind {
833 File,
835 #[serde(rename = "dir")]
840 Directory,
841}
842
843impl InodeKind {
844 pub const fn as_str(self) -> &'static str {
846 match self {
847 Self::File => "file",
848 Self::Directory => "dir",
849 }
850 }
851}
852
853impl fmt::Display for InodeKind {
854 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
855 f.write_str(self.as_str())
856 }
857}
858
859#[cfg(test)]
860mod tests {
861 use super::{
862 next_public_ordinal, BindingGeneration, ChangeSeq, CheckpointId, CommitId, ContentId,
863 ContentStoreId, InodeId, ManifestNo, MetadataSegmentId, NameKey, NamespaceId, RevisionNo,
864 RunNo, SnapshotId, UploadId, WalNo, WriterEpoch, WriterId, MAX_PUBLIC_INTEGER,
865 };
866 use crate::AttributeRevisionNo;
867 use std::collections::BTreeSet;
868
869 #[test]
870 fn public_ordinal_advancement_accepts_the_maximum_and_rejects_the_next_value() {
871 assert_eq!(
872 next_public_ordinal(MAX_PUBLIC_INTEGER - 1),
873 Some(MAX_PUBLIC_INTEGER)
874 );
875 assert_eq!(next_public_ordinal(MAX_PUBLIC_INTEGER), None);
876 }
877
878 #[test]
879 fn public_ordinal_inputs_must_fit_the_json_safe_integer_range() {
880 macro_rules! assert_range {
881 ($type:ty) => {{
882 let constructed = <$type>::parse(MAX_PUBLIC_INTEGER)
883 .expect("construct the maximum public ordinal");
884 assert_eq!(constructed.0, MAX_PUBLIC_INTEGER);
885
886 let construction_error = <$type>::parse(MAX_PUBLIC_INTEGER + 1)
887 .expect_err("reject a value above the public limit");
888 assert_eq!(
889 construction_error.to_string(),
890 "must be an integer from 0 through 9007199254740991"
891 );
892
893 let maximum = serde_json::from_str::<$type>(&MAX_PUBLIC_INTEGER.to_string())
894 .expect("deserialize the maximum public ordinal");
895 assert_eq!(maximum.0, MAX_PUBLIC_INTEGER);
896
897 let error = serde_json::from_str::<$type>(&(MAX_PUBLIC_INTEGER + 1).to_string())
898 .expect_err("ordinal above the public range");
899 assert!(
900 error
901 .to_string()
902 .contains("must be an integer from 0 through 9007199254740991"),
903 "unexpected range error: {error}"
904 );
905 }};
906 }
907
908 assert_range!(RevisionNo);
909 assert_range!(ChangeSeq);
910 assert_range!(AttributeRevisionNo);
911 assert_range!(ManifestNo);
912 assert_range!(WalNo);
913 assert_range!(RunNo);
914 assert_range!(WriterEpoch);
915
916 assert_eq!(
917 serde_json::from_str::<InodeId>(&(MAX_PUBLIC_INTEGER + 1).to_string())
918 .expect("inode ids retain the full u64 range"),
919 InodeId(MAX_PUBLIC_INTEGER + 1)
920 );
921 }
922
923 #[test]
924 fn namespace_id_parse_accepts_allowed_grammar() {
925 let long_id = format!("a{}", "b".repeat(127));
926 for value in ["demo", "demo-1", "demo_1", "demo.v1", &long_id] {
927 let parsed = NamespaceId::parse(value).expect("valid namespace_id");
928 assert_eq!(parsed.as_str(), value);
929 }
930 }
931
932 #[test]
933 fn namespace_id_parse_rejects_invalid_values() {
934 let long_id = format!("a{}", "b".repeat(128));
935 for value in [
936 "", "/", "a/b", ".", "..", " demo", "demo ", "demo\n", "demo?", "demo#", "demo%",
937 "Demo", &long_id,
938 ] {
939 assert!(
940 NamespaceId::parse(value).is_err(),
941 "expected invalid namespace_id {value:?}"
942 );
943 }
944 }
945
946 #[test]
947 fn namespace_id_parse_rejects_reserved_system_prefix() {
948 assert!(NamespaceId::parse("loonfs-doctor-abc").is_err());
949 assert!(NamespaceId::parse("loonfs-").is_err());
950 assert_eq!(
952 NamespaceId::parse("my-loonfs-notes")
953 .expect("non-prefixed use is allowed")
954 .as_str(),
955 "my-loonfs-notes"
956 );
957 assert!(CommitId::parse("loonfs-retry-1").is_ok());
959 }
960
961 #[test]
962 fn writer_id_rejects_blank_text() {
963 for value in ["", " ", "\n", " \t "] {
964 assert!(WriterId::parse(value).is_err(), "accepted {value:?}");
965 }
966 }
967
968 #[test]
969 fn binding_generation_requires_nonempty_lowercase_hex() {
970 for value in ["", "abcg", "ABC", "01-23"] {
971 assert!(
972 BindingGeneration::parse(value).is_err(),
973 "accepted {value:?}"
974 );
975 }
976 for value in ["0", "0123456789abcdef"] {
977 assert_eq!(
978 BindingGeneration::parse(value)
979 .expect("valid binding generation")
980 .as_str(),
981 value
982 );
983 }
984 }
985
986 #[test]
987 fn identity_try_from_validates_values() {
988 assert_eq!(
989 NamespaceId::try_from("demo")
990 .expect("valid namespace id")
991 .as_str(),
992 "demo"
993 );
994 assert_eq!(
995 CommitId::try_from("commit-1")
996 .expect("valid commit id")
997 .as_str(),
998 "commit-1"
999 );
1000 assert_eq!(
1001 ContentStoreId::try_from("cs_00000000000000000000000000000001")
1002 .expect("valid content store id")
1003 .as_str(),
1004 "cs_00000000000000000000000000000001"
1005 );
1006 assert_eq!(
1007 CheckpointId::try_from("pin_00000000000000000001-0000000000000001")
1008 .expect("valid checkpoint id")
1009 .as_str(),
1010 "pin_00000000000000000001-0000000000000001"
1011 );
1012 assert_eq!(
1013 NameKey::try_from("report.txt".to_owned())
1014 .expect("valid name key")
1015 .as_str(),
1016 "report.txt"
1017 );
1018
1019 assert!(NamespaceId::try_from("invalid/name").is_err());
1020 assert!(CommitId::try_from("invalid/name").is_err());
1021 assert!(ContentStoreId::try_from("cs_0000000000000000000000000000000g").is_err());
1022 assert!(CheckpointId::try_from("chk_0000000000000000000000000000000g").is_err());
1023 assert!(NameKey::try_from("a/b").is_err());
1024 }
1025
1026 #[test]
1027 fn identity_deserialize_validates_values() {
1028 let namespace_id: NamespaceId =
1029 serde_json::from_str(r#""demo""#).expect("valid namespace id json");
1030 assert_eq!(namespace_id.as_str(), "demo");
1031 let commit_id: CommitId =
1032 serde_json::from_str(r#""commit-1""#).expect("valid commit id json");
1033 assert_eq!(commit_id.as_str(), "commit-1");
1034 let content_store_id: ContentStoreId =
1035 serde_json::from_str(r#""cs_00000000000000000000000000000001""#)
1036 .expect("valid content store id json");
1037 assert_eq!(
1038 content_store_id.as_str(),
1039 "cs_00000000000000000000000000000001"
1040 );
1041 let checkpoint_id: CheckpointId =
1042 serde_json::from_str(r#""pin_00000000000000000001-0000000000000001""#)
1043 .expect("valid checkpoint id json");
1044 assert_eq!(
1045 checkpoint_id.as_str(),
1046 "pin_00000000000000000001-0000000000000001"
1047 );
1048
1049 let namespace_error = serde_json::from_str::<NamespaceId>(r#""invalid/name""#)
1050 .expect_err("invalid namespace id json");
1051 assert!(namespace_error.to_string().contains("namespace_id"));
1052 let commit_error = serde_json::from_str::<CommitId>(r#""invalid/name""#)
1053 .expect_err("invalid commit id json");
1054 assert!(commit_error.to_string().contains("commit_id"));
1055 let content_store_error =
1056 serde_json::from_str::<ContentStoreId>(r#""cs_0000000000000000000000000000000g""#)
1057 .expect_err("invalid content store id json");
1058 assert!(content_store_error.to_string().contains("generated id"));
1059 let checkpoint_error =
1060 serde_json::from_str::<CheckpointId>(r#""chk_0000000000000000000000000000000g""#)
1061 .expect_err("invalid checkpoint id json");
1062 assert!(checkpoint_error.to_string().contains("generated id"));
1063 }
1064
1065 #[test]
1066 fn generated_content_store_id_parse_requires_prefix_and_lower_hex_body() {
1067 let parsed = ContentStoreId::parse("cs_00000000000000000000000000000001")
1068 .expect("valid content store id");
1069
1070 assert_eq!(parsed.as_str(), "cs_00000000000000000000000000000001");
1071 let hyphenated_content_store_id = ["cs", "1"].join("-");
1072 for value in [
1073 hyphenated_content_store_id.as_str(),
1074 "upl_00000000000000000000000000000001",
1075 "content-stores/foo",
1076 "cs_",
1077 "cs_abcdef",
1078 "cs_0000000000000000000000000000000",
1079 "cs_000000000000000000000000000000001",
1080 "cs_ABCDEF00000000000000000000000000",
1081 "cs_0000000000000000000000000000000g",
1082 " cs_00000000000000000000000000000001",
1083 "cs_00000000000000000000000000000001 ",
1084 ] {
1085 assert!(
1086 ContentStoreId::parse(value).is_err(),
1087 "expected invalid content store id {value:?}"
1088 );
1089 }
1090 }
1091
1092 #[test]
1093 fn generated_upload_wal_metadata_segment_and_checkpoint_ids_reject_hyphenated_ids() {
1094 assert!(UploadId::parse("upl_00000000000000000000000000000001").is_ok());
1095 assert!(MetadataSegmentId::parse("seg_00000000000000000000000000000001").is_ok());
1096 assert!(CheckpointId::parse("pin_00000000000000000001-0000000000000001").is_ok());
1097 assert!(UploadId::parse(["upl", "123"].join("-")).is_err());
1098 assert!(MetadataSegmentId::parse(["seg", "123"].join("-")).is_err());
1101 assert!(CheckpointId::parse(["chk", "123"].join("-")).is_err());
1102 }
1103
1104 #[test]
1105 fn generated_runtime_ids_use_lower_hex_bodies() {
1106 let upload_id = UploadId::generate();
1107 let metadata_segment_id = MetadataSegmentId::generate();
1108 let checkpoint_id = CheckpointId::generate(ManifestNo(1));
1109
1110 assert_generated_id_shape(upload_id.as_str(), "upl");
1111 assert_generated_id_shape(metadata_segment_id.as_str(), "seg");
1112 assert_eq!(checkpoint_id.manifest_no(), ManifestNo(1));
1113 assert!(UploadId::parse(upload_id.as_str()).is_ok());
1114 assert!(MetadataSegmentId::parse(metadata_segment_id.as_str()).is_ok());
1115 assert!(CheckpointId::parse(checkpoint_id.as_str()).is_ok());
1116 }
1117
1118 #[test]
1119 fn checkpoint_ids_order_and_validate_their_manifest_numbers() {
1120 let first = CheckpointId::parse("pin_00000000000000000009-ffffffffffffffff").expect("pin");
1121 let second = CheckpointId::parse("pin_00000000000000000010-0000000000000000").expect("pin");
1122 assert!(first < second);
1123 assert_eq!(second.manifest_no(), ManifestNo(10));
1124 let snapshot_id = SnapshotId::from(second.clone());
1125 let decoded: SnapshotId = serde_json::from_str(
1126 &serde_json::to_string(&snapshot_id).expect("serialize snapshot id"),
1127 )
1128 .expect("decode snapshot id");
1129 assert_eq!(CheckpointId::from(decoded), second);
1130 for invalid in [
1131 "pin_00000000000000000000-0000000000000000".to_owned(),
1132 format!("pin_{:020}-0000000000000000", MAX_PUBLIC_INTEGER + 1),
1133 "pin_00000000000000000001-000000000000000G".to_owned(),
1134 "pin_1-0000000000000000".to_owned(),
1135 "pin_00000000000000000001-00000000000000000".to_owned(),
1136 ] {
1137 assert!(CheckpointId::parse(&invalid).is_err());
1138 assert!(SnapshotId::parse(&invalid).is_err());
1139 }
1140 }
1141
1142 #[test]
1143 fn generated_content_ids_are_unique_and_shard_uniformly() {
1144 let mut ids = BTreeSet::new();
1145 let mut first_level_shards = BTreeSet::new();
1146 let mut leaf_shards = BTreeSet::new();
1147 for _ in 0..512 {
1148 let id = ContentId::generate();
1149 assert_generated_id_shape(id.as_str(), "con");
1150 let [first, second] = id.shard_prefixes();
1151 assert_eq!(first, &id.as_str()["con_".len().."con_".len() + 2]);
1152 assert_eq!(second, &id.as_str()["con_".len() + 2.."con_".len() + 4]);
1153 first_level_shards.insert(first.to_owned());
1154 leaf_shards.insert(format!("{first}/{second}"));
1155 assert!(
1156 ids.insert(id.clone()),
1157 "generated duplicate content id {id}"
1158 );
1159 }
1160 assert!(
1164 first_level_shards.len() > 128,
1165 "content id first-level shards are not spread: {} distinct",
1166 first_level_shards.len()
1167 );
1168 assert!(
1169 leaf_shards.len() > 480,
1170 "content id leaf shards are not spread: {} distinct",
1171 leaf_shards.len()
1172 );
1173 }
1174
1175 #[test]
1176 fn content_id_parse_requires_the_generated_id_shape() {
1177 assert!(ContentId::parse("con_0123456789abcdef0123456789abcdef").is_ok());
1178 assert!(ContentId::parse("upl_0123456789abcdef0123456789abcdef").is_err());
1179 }
1180
1181 fn assert_generated_id_shape(value: &str, prefix: &str) {
1182 let expected_prefix = format!("{prefix}_");
1183 let body = value
1184 .strip_prefix(&expected_prefix)
1185 .expect("generated id prefix");
1186 assert_eq!(body.len(), 32);
1187 assert!(
1188 body.bytes()
1189 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)),
1190 "generated id body must be lowercase hex: {value}"
1191 );
1192 }
1193
1194 #[test]
1195 fn name_key_parse_rejects_invalid_values() {
1196 assert_eq!(
1197 NameKey::parse("").expect_err("empty").reason(),
1198 "must not be empty"
1199 );
1200 assert_eq!(
1201 NameKey::parse("a/b").expect_err("slash").reason(),
1202 "must not contain `/`"
1203 );
1204 assert_eq!(
1205 NameKey::parse(".").expect_err("dot").reason(),
1206 "must not be `.` or `..`"
1207 );
1208 assert_eq!(
1209 NameKey::parse("a\u{0}b").expect_err("control").reason(),
1210 "must not contain control characters"
1211 );
1212 NameKey::parse("k".repeat(super::MAX_NAME_KEY_BYTES)).expect("cap is inclusive");
1213 assert_eq!(
1214 NameKey::parse("k".repeat(super::MAX_NAME_KEY_BYTES + 1))
1215 .expect_err("over cap")
1216 .reason(),
1217 "exceeds the maximum name key length of 768 bytes"
1218 );
1219 }
1220
1221 #[test]
1222 fn name_key_serializes_as_string_and_validates_deserialize() {
1223 let name_key = NameKey::parse("report.txt").expect("valid name key");
1224
1225 assert_eq!(
1226 serde_json::to_string(&name_key).expect("serialize name key"),
1227 "\"report.txt\""
1228 );
1229 assert_eq!(
1230 serde_json::from_str::<NameKey>("\"report.txt\"").expect("deserialize name key"),
1231 name_key
1232 );
1233 assert!(serde_json::from_str::<NameKey>("\"a/b\"").is_err());
1234 }
1235}