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 fn value(&self) -> &str {
29 &self.value
30 }
31
32 pub fn reason(&self) -> &str {
34 &self.reason
35 }
36 }
37 };
38}
39
40validation_error!(
41 NamespaceIdValidationError,
42 "invalid namespace_id {value:?}: {reason}"
43);
44validation_error!(
45 CommitIdValidationError,
46 "invalid commit_id {value:?}: {reason}"
47);
48validation_error!(
49 GeneratedIdValidationError,
50 "invalid generated id {value:?}: {reason}"
51);
52validation_error!(
53 NameKeyValidationError,
54 "invalid name_key {value:?}: {reason}"
55);
56
57macro_rules! string_id {
84 (
85 $(#[$meta:meta])*
86 $name:ident,
87 error = $error:ty,
88 validate = $validate:expr
89 $(, schema($($schema:tt)+))?
90 $(,)?
91 ) => {
92 $(#[$meta])*
93 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
94 #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
95 #[cfg_attr(
96 feature = "openapi",
97 schema(value_type = String $(, $($schema)+)?)
98 )]
99 pub struct $name(String);
100
101 impl $name {
102 pub fn parse(value: impl AsRef<str>) -> Result<Self, $error> {
104 let value = value.as_ref();
105 ($validate)(value)?;
106 Ok(Self(value.to_owned()))
107 }
108
109 pub fn as_str(&self) -> &str {
111 &self.0
112 }
113 }
114
115 impl TryFrom<&str> for $name {
116 type Error = $error;
117
118 fn try_from(value: &str) -> Result<Self, Self::Error> {
119 Self::parse(value)
120 }
121 }
122
123 impl TryFrom<String> for $name {
124 type Error = $error;
125
126 fn try_from(value: String) -> Result<Self, Self::Error> {
127 Self::parse(value)
128 }
129 }
130
131 impl std::str::FromStr for $name {
132 type Err = $error;
133
134 fn from_str(value: &str) -> Result<Self, Self::Err> {
135 Self::parse(value)
136 }
137 }
138
139 impl AsRef<str> for $name {
140 fn as_ref(&self) -> &str {
141 self.as_str()
142 }
143 }
144
145 impl std::borrow::Borrow<str> for $name {
146 fn borrow(&self) -> &str {
147 self.as_str()
148 }
149 }
150
151 impl std::fmt::Display for $name {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 f.write_str(&self.0)
154 }
155 }
156
157 impl serde::Serialize for $name {
158 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
159 where
160 S: serde::Serializer,
161 {
162 serializer.serialize_str(&self.0)
163 }
164 }
165
166 impl<'de> serde::Deserialize<'de> for $name {
167 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
168 where
169 D: serde::Deserializer<'de>,
170 {
171 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
172 Self::parse(value).map_err(serde::de::Error::custom)
173 }
174 }
175 };
176 (
177 $(#[$meta:meta])*
178 $name:ident,
179 prefix = $prefix:literal
180 $(, schema($($schema:tt)+))?
181 $(,)?
182 ) => {
183 string_id! {
184 $(#[$meta])*
185 $name,
186 error = GeneratedIdValidationError,
187 validate = |value: &str| validate_generated_id($prefix, value)
188 $(, schema($($schema)+))?
189 }
190
191 impl $name {
192 pub fn generate() -> Self {
194 Self(generated_id($prefix))
195 }
196 }
197 };
198}
199
200macro_rules! numeric_id {
207 (
208 $(#[$meta:meta])*
209 $name:ident,
210 public_ordinal,
211 schema_description = $schema_description:literal
212 ) => {
213 $(#[$meta])*
214 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
215 pub struct $name(pub u64);
216
217 impl $name {
218 pub fn parse(value: u64) -> Result<Self, $crate::PublicOrdinalRangeError> {
225 if value > $crate::MAX_PUBLIC_INTEGER {
226 return Err($crate::PublicOrdinalRangeError);
227 }
228 Ok(Self(value))
229 }
230 }
231
232 #[cfg(feature = "openapi")]
233 impl utoipa::PartialSchema for $name {
234 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
235 utoipa::openapi::schema::Object::builder()
236 .schema_type(utoipa::openapi::schema::Type::Integer)
237 .format(Some(utoipa::openapi::SchemaFormat::KnownFormat(
238 utoipa::openapi::KnownFormat::Int64,
239 )))
240 .minimum(Some(0u64))
241 .maximum(Some($crate::MAX_PUBLIC_INTEGER))
242 .description(Some($schema_description))
243 .into()
244 }
245 }
246
247 #[cfg(feature = "openapi")]
248 impl utoipa::ToSchema for $name {}
249
250 impl<'de> serde::Deserialize<'de> for $name {
251 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
252 where
253 D: serde::Deserializer<'de>,
254 {
255 let value = <u64 as serde::Deserialize>::deserialize(deserializer)?;
256 Self::parse(value).map_err(serde::de::Error::custom)
257 }
258 }
259
260 impl From<u64> for $name {
261 fn from(value: u64) -> Self {
262 Self(value)
263 }
264 }
265
266 impl fmt::Display for $name {
267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268 write!(f, "{}", self.0)
269 }
270 }
271 };
272 (
273 $(#[$meta:meta])*
274 $name:ident
275 ) => {
276 $(#[$meta])*
277 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
278 #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
279 #[cfg_attr(feature = "openapi", schema(value_type = u64))]
280 pub struct $name(pub u64);
281
282 impl From<u64> for $name {
283 fn from(value: u64) -> Self {
284 Self(value)
285 }
286 }
287
288 impl fmt::Display for $name {
289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 write!(f, "{}", self.0)
291 }
292 }
293 };
294}
295
296pub(crate) use numeric_id;
297pub(crate) use string_id;
298pub(crate) use validation_error;
299
300pub fn generated_id(prefix: &'static str) -> String {
313 format!("{prefix}_{}", hex_encode_bytes(&random_128()))
314}
315
316fn generated_position_suffix() -> String {
317 let suffix = hex_encode_bytes(&random_128());
318 suffix[..16].to_owned()
319}
320
321fn random_128() -> [u8; 16] {
326 let mut bytes = [0_u8; 16];
327 getrandom::fill(&mut bytes).expect("the system random generator must be available");
328 bytes
329}
330
331fn validate_generated_id(
332 prefix: &'static str,
333 value: &str,
334) -> Result<(), GeneratedIdValidationError> {
335 let expected_prefix = format!("{prefix}_");
336 let Some(body) = value.strip_prefix(&expected_prefix) else {
337 return Err(generated_id_error(
338 value,
339 format!("must start with `{expected_prefix}`"),
340 ));
341 };
342 if body.len() != SERVER_GENERATED_ID_BODY_LEN {
343 return Err(generated_id_error(
344 value,
345 format!("body must be {SERVER_GENERATED_ID_BODY_LEN} lowercase hex characters"),
346 ));
347 }
348 if !body.bytes().all(is_lower_hex_byte) {
349 return Err(generated_id_error(
350 value,
351 "body must contain only lowercase hex characters".to_owned(),
352 ));
353 }
354 Ok(())
355}
356
357fn validate_namespace_id(value: &str) -> Result<(), NamespaceIdValidationError> {
358 validate_id_grammar(value).map_err(|reason| namespace_id_error(value, reason))?;
359 if value.starts_with("loonfs-") {
362 return Err(namespace_id_error(
363 value,
364 "the `loonfs-` prefix is reserved for LoonFS system namespaces",
365 ));
366 }
367 Ok(())
368}
369
370fn validate_commit_id(value: &str) -> Result<(), CommitIdValidationError> {
371 validate_id_grammar(value).map_err(|reason| commit_id_error(value, reason))
372}
373
374pub const MAX_NAME_KEY_BYTES: usize = 768;
379pub const MAX_ID_BYTES: usize = 128;
381
382fn validate_name_key(value: &str) -> Result<(), NameKeyValidationError> {
383 if value.is_empty() {
384 return Err(name_key_error(value, "must not be empty"));
385 }
386 if value.contains('/') {
387 return Err(name_key_error(value, "must not contain `/`"));
388 }
389 if matches!(value, "." | "..") {
390 return Err(name_key_error(value, "must not be `.` or `..`"));
391 }
392 if value.chars().any(|character| character.is_control()) {
393 return Err(name_key_error(value, "must not contain control characters"));
394 }
395 if value.len() > MAX_NAME_KEY_BYTES {
396 return Err(name_key_error(
398 "",
399 format!("exceeds the maximum name key length of {MAX_NAME_KEY_BYTES} bytes"),
400 ));
401 }
402 Ok(())
403}
404
405fn parse_position_suffix_id<'a>(
411 prefix: &str,
412 position_field: &str,
413 value: &'a str,
414) -> Result<(&'a str, &'a str), GeneratedIdValidationError> {
415 let Some((position, suffix)) = value
416 .strip_prefix(prefix)
417 .and_then(|rest| rest.strip_prefix('_'))
418 .and_then(|rest| rest.split_once('-'))
419 else {
420 return Err(generated_id_error(
421 value,
422 format!("must be `{prefix}_<20 digit {position_field}>-<16 lowercase hex>`"),
423 ));
424 };
425 if position.len() != 20 || !position.bytes().all(|byte| byte.is_ascii_digit()) {
426 return Err(generated_id_error(
427 value,
428 format!("`{position_field}` must be 20 decimal digits"),
429 ));
430 }
431 if suffix.len() != 16 || !suffix.bytes().all(is_lower_hex_byte) {
432 return Err(generated_id_error(
433 value,
434 "suffix must be 16 lowercase hex characters".to_owned(),
435 ));
436 }
437 Ok((position, suffix))
438}
439
440fn validate_id_grammar(value: &str) -> Result<(), String> {
441 if value.is_empty() {
442 return Err("must not be empty".to_owned());
443 }
444 if value.len() > MAX_ID_BYTES {
445 return Err(format!("must be {MAX_ID_BYTES} bytes or fewer"));
446 }
447 if value.trim() != value {
448 return Err("must not have leading or trailing whitespace".to_owned());
449 }
450 if matches!(value, "." | "..") {
451 return Err("must not be `.` or `..`".to_owned());
452 }
453
454 let mut chars = value.chars();
455 let first = chars
456 .next()
457 .expect("empty id returned before char validation");
458 if !first.is_ascii_lowercase() && !first.is_ascii_digit() {
459 return Err("must start with a lowercase ASCII letter or digit".to_owned());
460 }
461 if !chars.all(is_allowed_id_tail_char) {
462 return Err(
463 "must contain only lowercase ASCII letters, digits, `.`, `_`, or `-`".to_owned(),
464 );
465 }
466
467 Ok(())
468}
469
470fn is_allowed_id_tail_char(ch: char) -> bool {
471 ch.is_ascii_lowercase() || ch.is_ascii_digit() || matches!(ch, '.' | '_' | '-')
472}
473
474fn namespace_id_error(value: &str, reason: impl Into<String>) -> NamespaceIdValidationError {
475 NamespaceIdValidationError {
476 value: value.to_owned(),
477 reason: reason.into(),
478 }
479}
480
481fn commit_id_error(value: &str, reason: impl Into<String>) -> CommitIdValidationError {
482 CommitIdValidationError {
483 value: value.to_owned(),
484 reason: reason.into(),
485 }
486}
487
488fn generated_id_error(value: &str, reason: String) -> GeneratedIdValidationError {
489 GeneratedIdValidationError {
490 value: value.to_owned(),
491 reason,
492 }
493}
494
495fn name_key_error(value: &str, reason: impl Into<String>) -> NameKeyValidationError {
496 NameKeyValidationError {
497 value: value.to_owned(),
498 reason: reason.into(),
499 }
500}
501
502string_id! {
507 NamespaceId,
514 error = NamespaceIdValidationError,
515 validate = validate_namespace_id,
516 schema(
517 pattern = r"^[a-z0-9][a-z0-9._-]{0,127}$",
521 example = "demo"
522 )
523}
524
525string_id! {
526 ContentStoreId,
530 prefix = "cs"
531}
532
533string_id! {
534 CommitId,
541 error = CommitIdValidationError,
542 validate = validate_commit_id,
543 schema(
544 pattern = r"^[a-z0-9][a-z0-9._-]{0,127}$",
545 example = "c_f3a9c2d4b6e8417a90c5d2f8e1b7a6c0"
546 )
547}
548
549impl CommitId {
550 pub fn generate() -> Self {
552 Self(generated_id("c"))
553 }
554}
555
556string_id! {
557 CheckpointId,
561 prefix = "chk",
562 schema(
563 pattern = r"^chk_[0-9a-f]{32}$",
564 example = "chk_00000000000000000000000000000002"
565 )
566}
567
568string_id! {
569 UploadId,
571 prefix = "upl",
572 schema(
573 pattern = r"^upl_[0-9a-f]{32}$",
574 example = "upl_4d8f2c91a7b34e0f9c6d1a2b3e5f708c"
575 )
576}
577
578string_id! {
579 ContentId,
587 prefix = "con",
588 schema(
589 pattern = r"^con_[0-9a-f]{32}$",
590 example = "con_9f2a6c0e4b7d4a90b13f0d8c5e6a2b41"
591 )
592}
593
594impl ContentId {
595 pub fn shard_prefixes(&self) -> [&str; CONTENT_ID_SHARD_LEVELS] {
600 let first_start = CONTENT_ID_PREFIX_LEN;
601 let second_start = first_start + CONTENT_ID_SHARD_WIDTH;
602 [
603 &self.0[first_start..second_start],
604 &self.0[second_start..second_start + CONTENT_ID_SHARD_WIDTH],
605 ]
606 }
607}
608
609const CONTENT_ID_PREFIX_LEN: usize = "con_".len();
611const CONTENT_ID_SHARD_LEVELS: usize = 2;
613const CONTENT_ID_SHARD_WIDTH: usize = 2;
615
616string_id! {
617 MetadataSegmentId,
619 prefix = "seg"
620}
621
622string_id! {
623 MetadataCompactionId,
629 prefix = "cmp"
630}
631
632string_id! {
633 IndexSegmentId,
635 prefix = "idx"
636}
637
638string_id! {
639 GrepManifestObjectId,
641 prefix = "gmf"
642}
643
644const MANIFEST_OBJECT_ID_PREFIX: &str = "man";
646const MANIFEST_OBJECT_ID_POSITION_FIELD: &str = "manifest_no";
648
649string_id! {
650 ManifestObjectId,
652 error = GeneratedIdValidationError,
653 validate = |value: &str| {
654 parse_position_suffix_id(
655 MANIFEST_OBJECT_ID_PREFIX,
656 MANIFEST_OBJECT_ID_POSITION_FIELD,
657 value,
658 )
659 .map(|_| ())
660 }
661}
662
663impl ManifestObjectId {
664 pub fn generate(manifest_no: ManifestNo) -> Self {
666 Self(format!(
667 "{MANIFEST_OBJECT_ID_PREFIX}_{:020}-{}",
668 manifest_no.0,
669 generated_position_suffix()
670 ))
671 }
672}
673
674pub fn manifest_object_id_manifest_no(object_id: &str) -> Option<ManifestNo> {
676 let (position, _) = parse_position_suffix_id(
677 MANIFEST_OBJECT_ID_PREFIX,
678 MANIFEST_OBJECT_ID_POSITION_FIELD,
679 object_id,
680 )
681 .ok()?;
682 position
683 .parse()
684 .ok()
685 .and_then(|value| ManifestNo::parse(value).ok())
686}
687
688const WAL_SEGMENT_ID_PREFIX: &str = "wal";
690const WAL_SEGMENT_ID_POSITION_FIELD: &str = "start_seq";
692
693string_id! {
694 WalSegmentId,
696 error = GeneratedIdValidationError,
697 validate = |value: &str| {
698 parse_position_suffix_id(WAL_SEGMENT_ID_PREFIX, WAL_SEGMENT_ID_POSITION_FIELD, value)
699 .map(|_| ())
700 }
701}
702
703impl WalSegmentId {
704 pub fn generate(start_seq: ChangeSeq) -> Self {
714 Self(format!(
715 "{WAL_SEGMENT_ID_PREFIX}_{:020}-{}",
716 start_seq.0,
717 generated_position_suffix()
718 ))
719 }
720}
721
722pub fn wal_segment_id_start_seq(segment_id: &str) -> Option<ChangeSeq> {
729 let (position, _) = parse_position_suffix_id(
730 WAL_SEGMENT_ID_PREFIX,
731 WAL_SEGMENT_ID_POSITION_FIELD,
732 segment_id,
733 )
734 .ok()?;
735 position
736 .parse()
737 .ok()
738 .and_then(|value| ChangeSeq::parse(value).ok())
739}
740
741string_id! {
742 NameKey,
747 error = NameKeyValidationError,
748 validate = validate_name_key,
749 schema(example = "report.txt")
750}
751
752impl NameKey {
753 pub fn for_display_name(display_name: &crate::DisplayName) -> Self {
755 Self(crate::name_key_for_display_name(display_name.as_str()))
756 }
757}
758
759pub const MAX_PUBLIC_INTEGER: u64 = 9_007_199_254_740_991;
768
769#[derive(Debug, Clone, Copy, PartialEq, Eq)]
771pub struct PublicOrdinalRangeError;
772
773impl fmt::Display for PublicOrdinalRangeError {
774 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
775 write!(f, "must be an integer from 0 through {MAX_PUBLIC_INTEGER}")
776 }
777}
778
779impl std::error::Error for PublicOrdinalRangeError {}
780
781pub fn next_public_ordinal(current: u64) -> Option<u64> {
783 current
784 .checked_add(1)
785 .filter(|next| *next <= MAX_PUBLIC_INTEGER)
786}
787
788numeric_id! {
789 InodeId
793}
794
795pub const ROOT_INODE_ID: InodeId = InodeId(1);
797
798pub const FIRST_ALLOCATABLE_INODE_ID: InodeId = InodeId(ROOT_INODE_ID.0 + 1);
800
801numeric_id! {
802 RevisionNo,
804 public_ordinal,
805 schema_description = "Revision number for a file's content. It increases whenever the content is replaced or restored."
806}
807
808numeric_id! {
809 ChangeSeq,
813 public_ordinal,
814 schema_description = "Sequence number assigned to a namespace commit. It determines the order in which commits become visible."
815}
816
817numeric_id! {
818 ManifestNo,
823 public_ordinal,
824 schema_description = "Monotonic manifest counter for one namespace. It can increase when metadata changes, even if no namespace commit is written."
825}
826
827numeric_id! {
828 RunNo,
834 public_ordinal,
835 schema_description = "Monotonic run counter allocated by the manifest that names the run. A run is the set of segments one producer wrote together."
836}
837
838impl RunNo {
839 pub fn successor(self) -> Result<Self, PublicOrdinalRangeError> {
841 next_public_ordinal(self.0)
842 .map(Self)
843 .ok_or(PublicOrdinalRangeError)
844 }
845}
846
847numeric_id! {
848 WriterEpoch,
850 public_ordinal,
851 schema_description = "Counter used to reject writes from an older writer."
852}
853
854#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
860#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
861#[serde(rename_all = "snake_case")]
862pub enum InodeKind {
863 File,
865 #[serde(rename = "dir")]
870 Directory,
871}
872
873impl InodeKind {
874 pub const fn as_str(self) -> &'static str {
876 match self {
877 Self::File => "file",
878 Self::Directory => "dir",
879 }
880 }
881}
882
883impl fmt::Display for InodeKind {
884 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
885 f.write_str(self.as_str())
886 }
887}
888
889#[cfg(test)]
890mod tests {
891 use super::{
892 next_public_ordinal, ChangeSeq, CheckpointId, CommitId, ContentId, ContentStoreId, InodeId,
893 ManifestNo, ManifestObjectId, MetadataSegmentId, NameKey, NamespaceId, RevisionNo, RunNo,
894 UploadId, WalSegmentId, WriterEpoch, MAX_PUBLIC_INTEGER,
895 };
896 use crate::AttributeRevisionNo;
897 use std::collections::BTreeSet;
898
899 #[test]
900 fn public_ordinal_advancement_accepts_the_maximum_and_rejects_the_next_value() {
901 assert_eq!(
902 next_public_ordinal(MAX_PUBLIC_INTEGER - 1),
903 Some(MAX_PUBLIC_INTEGER)
904 );
905 assert_eq!(next_public_ordinal(MAX_PUBLIC_INTEGER), None);
906 }
907
908 #[test]
909 fn public_ordinal_inputs_must_fit_the_json_safe_integer_range() {
910 macro_rules! assert_range {
911 ($type:ty) => {{
912 let constructed = <$type>::parse(MAX_PUBLIC_INTEGER)
913 .expect("construct the maximum public ordinal");
914 assert_eq!(constructed.0, MAX_PUBLIC_INTEGER);
915
916 let construction_error = <$type>::parse(MAX_PUBLIC_INTEGER + 1)
917 .expect_err("reject a value above the public limit");
918 assert_eq!(
919 construction_error.to_string(),
920 "must be an integer from 0 through 9007199254740991"
921 );
922
923 let maximum = serde_json::from_str::<$type>(&MAX_PUBLIC_INTEGER.to_string())
924 .expect("deserialize the maximum public ordinal");
925 assert_eq!(maximum.0, MAX_PUBLIC_INTEGER);
926
927 let error = serde_json::from_str::<$type>(&(MAX_PUBLIC_INTEGER + 1).to_string())
928 .expect_err("ordinal above the public range");
929 assert!(
930 error
931 .to_string()
932 .contains("must be an integer from 0 through 9007199254740991"),
933 "unexpected range error: {error}"
934 );
935 }};
936 }
937
938 assert_range!(RevisionNo);
939 assert_range!(ChangeSeq);
940 assert_range!(AttributeRevisionNo);
941 assert_range!(ManifestNo);
942 assert_range!(RunNo);
943 assert_range!(WriterEpoch);
944
945 assert_eq!(
946 serde_json::from_str::<InodeId>(&(MAX_PUBLIC_INTEGER + 1).to_string())
947 .expect("inode ids retain the full u64 range"),
948 InodeId(MAX_PUBLIC_INTEGER + 1)
949 );
950 }
951
952 #[test]
953 fn namespace_id_parse_accepts_allowed_grammar() {
954 let long_id = format!("a{}", "b".repeat(127));
955 for value in ["demo", "demo-1", "demo_1", "demo.v1", &long_id] {
956 let parsed = NamespaceId::parse(value).expect("valid namespace_id");
957 assert_eq!(parsed.as_str(), value);
958 }
959 }
960
961 #[test]
962 fn namespace_id_parse_rejects_invalid_values() {
963 let long_id = format!("a{}", "b".repeat(128));
964 for value in [
965 "", "/", "a/b", ".", "..", " demo", "demo ", "demo\n", "demo?", "demo#", "demo%",
966 "Demo", &long_id,
967 ] {
968 assert!(
969 NamespaceId::parse(value).is_err(),
970 "expected invalid namespace_id {value:?}"
971 );
972 }
973 }
974
975 #[test]
976 fn namespace_id_parse_rejects_reserved_system_prefix() {
977 assert!(NamespaceId::parse("loonfs-doctor-abc").is_err());
978 assert!(NamespaceId::parse("loonfs-").is_err());
979 assert_eq!(
981 NamespaceId::parse("my-loonfs-notes")
982 .expect("non-prefixed use is allowed")
983 .as_str(),
984 "my-loonfs-notes"
985 );
986 assert!(CommitId::parse("loonfs-retry-1").is_ok());
988 }
989
990 #[test]
991 fn identity_try_from_validates_values() {
992 assert_eq!(
993 NamespaceId::try_from("demo")
994 .expect("valid namespace id")
995 .as_str(),
996 "demo"
997 );
998 assert_eq!(
999 CommitId::try_from("commit-1")
1000 .expect("valid commit id")
1001 .as_str(),
1002 "commit-1"
1003 );
1004 assert_eq!(
1005 ContentStoreId::try_from("cs_00000000000000000000000000000001")
1006 .expect("valid content store id")
1007 .as_str(),
1008 "cs_00000000000000000000000000000001"
1009 );
1010 assert_eq!(
1011 CheckpointId::try_from("chk_00000000000000000000000000000001")
1012 .expect("valid checkpoint id")
1013 .as_str(),
1014 "chk_00000000000000000000000000000001"
1015 );
1016 assert_eq!(
1017 NameKey::try_from("report.txt".to_owned())
1018 .expect("valid name key")
1019 .as_str(),
1020 "report.txt"
1021 );
1022 assert_eq!(
1023 ManifestObjectId::try_from("man_00000000000000000042-0123456789abcdef")
1024 .expect("valid manifest object id")
1025 .as_str(),
1026 "man_00000000000000000042-0123456789abcdef"
1027 );
1028
1029 assert!(NamespaceId::try_from("invalid/name").is_err());
1030 assert!(CommitId::try_from("invalid/name").is_err());
1031 assert!(ContentStoreId::try_from("cs_0000000000000000000000000000000g").is_err());
1032 assert!(CheckpointId::try_from("chk_0000000000000000000000000000000g").is_err());
1033 assert!(NameKey::try_from("a/b").is_err());
1034 assert!(ManifestObjectId::try_from("42-0123456789abcdef").is_err());
1035 }
1036
1037 #[test]
1038 fn identity_deserialize_validates_values() {
1039 let namespace_id: NamespaceId =
1040 serde_json::from_str(r#""demo""#).expect("valid namespace id json");
1041 assert_eq!(namespace_id.as_str(), "demo");
1042 let commit_id: CommitId =
1043 serde_json::from_str(r#""commit-1""#).expect("valid commit id json");
1044 assert_eq!(commit_id.as_str(), "commit-1");
1045 let content_store_id: ContentStoreId =
1046 serde_json::from_str(r#""cs_00000000000000000000000000000001""#)
1047 .expect("valid content store id json");
1048 assert_eq!(
1049 content_store_id.as_str(),
1050 "cs_00000000000000000000000000000001"
1051 );
1052 let checkpoint_id: CheckpointId =
1053 serde_json::from_str(r#""chk_00000000000000000000000000000001""#)
1054 .expect("valid checkpoint id json");
1055 assert_eq!(
1056 checkpoint_id.as_str(),
1057 "chk_00000000000000000000000000000001"
1058 );
1059
1060 let namespace_error = serde_json::from_str::<NamespaceId>(r#""invalid/name""#)
1061 .expect_err("invalid namespace id json");
1062 assert!(namespace_error.to_string().contains("namespace_id"));
1063 let commit_error = serde_json::from_str::<CommitId>(r#""invalid/name""#)
1064 .expect_err("invalid commit id json");
1065 assert!(commit_error.to_string().contains("commit_id"));
1066 let content_store_error =
1067 serde_json::from_str::<ContentStoreId>(r#""cs_0000000000000000000000000000000g""#)
1068 .expect_err("invalid content store id json");
1069 assert!(content_store_error.to_string().contains("generated id"));
1070 let checkpoint_error =
1071 serde_json::from_str::<CheckpointId>(r#""chk_0000000000000000000000000000000g""#)
1072 .expect_err("invalid checkpoint id json");
1073 assert!(checkpoint_error.to_string().contains("generated id"));
1074 }
1075
1076 #[test]
1077 fn generated_content_store_id_parse_requires_prefix_and_lower_hex_body() {
1078 let parsed = ContentStoreId::parse("cs_00000000000000000000000000000001")
1079 .expect("valid content store id");
1080
1081 assert_eq!(parsed.as_str(), "cs_00000000000000000000000000000001");
1082 let hyphenated_content_store_id = ["cs", "1"].join("-");
1083 for value in [
1084 hyphenated_content_store_id.as_str(),
1085 "upl_00000000000000000000000000000001",
1086 "content-stores/foo",
1087 "cs_",
1088 "cs_abcdef",
1089 "cs_0000000000000000000000000000000",
1090 "cs_000000000000000000000000000000001",
1091 "cs_ABCDEF00000000000000000000000000",
1092 "cs_0000000000000000000000000000000g",
1093 " cs_00000000000000000000000000000001",
1094 "cs_00000000000000000000000000000001 ",
1095 ] {
1096 assert!(
1097 ContentStoreId::parse(value).is_err(),
1098 "expected invalid content store id {value:?}"
1099 );
1100 }
1101 }
1102
1103 #[test]
1104 fn generated_upload_wal_metadata_segment_and_checkpoint_ids_reject_hyphenated_ids() {
1105 assert!(UploadId::parse("upl_00000000000000000000000000000001").is_ok());
1106 assert!(MetadataSegmentId::parse("seg_00000000000000000000000000000001").is_ok());
1107 assert!(CheckpointId::parse("chk_00000000000000000000000000000001").is_ok());
1108 assert!(UploadId::parse(["upl", "123"].join("-")).is_err());
1109 assert!(WalSegmentId::parse("wal_00000000000000000412-9f2a6c0e4b7d4a90").is_ok());
1110 assert!(ManifestObjectId::parse("man_00000000000000000412-9f2a6c0e4b7d4a90").is_ok());
1111 assert!(WalSegmentId::parse("wal_412-9f2a6c0e4b7d4a90").is_err());
1112 assert!(WalSegmentId::parse("wal_00000000000000000412-9F2A6C0E4B7D4A90").is_err());
1113 assert!(ManifestObjectId::parse("man_412-9f2a6c0e4b7d4a90").is_err());
1114 assert!(ManifestObjectId::parse("man_00000000000000000412-9F2A6C0E4B7D4A90").is_err());
1115 assert!(ManifestObjectId::parse("mf_9f2a6c0e4b7d4a90b13f0d8c5e6a2b41").is_err());
1116 assert!(WalSegmentId::parse("seg_9f2a6c0e4b7d4a90b13f0d8c5e6a2b41").is_err());
1117 assert!(WalSegmentId::parse("00000000000000000412-9f2a6c0e4b7d4a90").is_err());
1120 assert!(ManifestObjectId::parse("00000000000000000412-9f2a6c0e4b7d4a90").is_err());
1121 assert!(WalSegmentId::parse("man_00000000000000000412-9f2a6c0e4b7d4a90").is_err());
1122 assert!(ManifestObjectId::parse("wal_00000000000000000412-9f2a6c0e4b7d4a90").is_err());
1123 assert!(MetadataSegmentId::parse(["seg", "123"].join("-")).is_err());
1124 assert!(CheckpointId::parse(["chk", "123"].join("-")).is_err());
1125 }
1126
1127 #[test]
1128 fn generated_runtime_ids_use_lower_hex_bodies() {
1129 let upload_id = UploadId::generate();
1130 let wal_segment_id = WalSegmentId::generate(ChangeSeq(412));
1131 let manifest_object_id = ManifestObjectId::generate(ManifestNo(413));
1132 let metadata_segment_id = MetadataSegmentId::generate();
1133 let checkpoint_id = CheckpointId::generate();
1134
1135 assert_generated_id_shape(upload_id.as_str(), "upl");
1136 assert!(wal_segment_id
1137 .as_str()
1138 .starts_with("wal_00000000000000000412-"));
1139 assert!(manifest_object_id
1140 .as_str()
1141 .starts_with("man_00000000000000000413-"));
1142 assert_generated_id_shape(metadata_segment_id.as_str(), "seg");
1143 assert_generated_id_shape(checkpoint_id.as_str(), "chk");
1144 assert!(UploadId::parse(upload_id.as_str()).is_ok());
1145 assert!(WalSegmentId::parse(wal_segment_id.as_str()).is_ok());
1146 assert!(ManifestObjectId::parse(manifest_object_id.as_str()).is_ok());
1147 assert!(MetadataSegmentId::parse(metadata_segment_id.as_str()).is_ok());
1148 assert!(CheckpointId::parse(checkpoint_id.as_str()).is_ok());
1149 }
1150
1151 #[test]
1152 fn generated_positional_ids_are_not_reused_across_samples() {
1153 let mut wal_segment_ids = BTreeSet::new();
1154 let mut manifest_object_ids = BTreeSet::new();
1155 for _ in 0..128 {
1156 let wal_segment_id = WalSegmentId::generate(ChangeSeq(412));
1157 assert!(
1158 wal_segment_ids.insert(wal_segment_id.clone()),
1159 "generated duplicate WAL segment id {wal_segment_id}"
1160 );
1161 let manifest_object_id = ManifestObjectId::generate(ManifestNo(412));
1162 assert!(
1163 manifest_object_ids.insert(manifest_object_id.clone()),
1164 "generated duplicate manifest object id {manifest_object_id}"
1165 );
1166 }
1167 }
1168
1169 #[test]
1170 fn wal_segment_id_start_seq_reads_the_position_digits() {
1171 assert_eq!(
1172 super::wal_segment_id_start_seq("wal_00000000000000000412-9f2a6c0e4b7d4a90"),
1173 Some(ChangeSeq(412))
1174 );
1175 assert_eq!(super::wal_segment_id_start_seq("not-a-segment-id"), None);
1176 assert_eq!(
1177 super::wal_segment_id_start_seq("wal_00009007199254740992-9f2a6c0e4b7d4a90"),
1178 None
1179 );
1180 }
1181
1182 #[test]
1183 fn manifest_object_id_manifest_no_reads_the_position_digits() {
1184 assert_eq!(
1185 super::manifest_object_id_manifest_no("man_00000000000000000412-9f2a6c0e4b7d4a90"),
1186 Some(ManifestNo(412))
1187 );
1188 assert_eq!(
1189 super::manifest_object_id_manifest_no("not-a-manifest-object-id"),
1190 None
1191 );
1192 assert_eq!(
1193 super::manifest_object_id_manifest_no("man_00009007199254740992-9f2a6c0e4b7d4a90"),
1194 None
1195 );
1196 }
1197
1198 #[test]
1199 fn generated_content_ids_are_unique_and_shard_uniformly() {
1200 let mut ids = BTreeSet::new();
1201 let mut first_level_shards = BTreeSet::new();
1202 let mut leaf_shards = BTreeSet::new();
1203 for _ in 0..512 {
1204 let id = ContentId::generate();
1205 assert_generated_id_shape(id.as_str(), "con");
1206 let [first, second] = id.shard_prefixes();
1207 assert_eq!(first, &id.as_str()["con_".len().."con_".len() + 2]);
1208 assert_eq!(second, &id.as_str()["con_".len() + 2.."con_".len() + 4]);
1209 first_level_shards.insert(first.to_owned());
1210 leaf_shards.insert(format!("{first}/{second}"));
1211 assert!(
1212 ids.insert(id.clone()),
1213 "generated duplicate content id {id}"
1214 );
1215 }
1216 assert!(
1220 first_level_shards.len() > 128,
1221 "content id first-level shards are not spread: {} distinct",
1222 first_level_shards.len()
1223 );
1224 assert!(
1225 leaf_shards.len() > 480,
1226 "content id leaf shards are not spread: {} distinct",
1227 leaf_shards.len()
1228 );
1229 }
1230
1231 #[test]
1232 fn content_id_parse_requires_the_generated_id_shape() {
1233 assert!(ContentId::parse("con_0123456789abcdef0123456789abcdef").is_ok());
1234 assert!(ContentId::parse("upl_0123456789abcdef0123456789abcdef").is_err());
1235 }
1236
1237 fn assert_generated_id_shape(value: &str, prefix: &str) {
1238 let expected_prefix = format!("{prefix}_");
1239 let body = value
1240 .strip_prefix(&expected_prefix)
1241 .expect("generated id prefix");
1242 assert_eq!(body.len(), 32);
1243 assert!(
1244 body.bytes()
1245 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)),
1246 "generated id body must be lowercase hex: {value}"
1247 );
1248 }
1249
1250 #[test]
1251 fn name_key_parse_rejects_invalid_values() {
1252 assert_eq!(
1253 NameKey::parse("").expect_err("empty").reason(),
1254 "must not be empty"
1255 );
1256 assert_eq!(
1257 NameKey::parse("a/b").expect_err("slash").reason(),
1258 "must not contain `/`"
1259 );
1260 assert_eq!(
1261 NameKey::parse(".").expect_err("dot").reason(),
1262 "must not be `.` or `..`"
1263 );
1264 assert_eq!(
1265 NameKey::parse("a\u{0}b").expect_err("control").reason(),
1266 "must not contain control characters"
1267 );
1268 NameKey::parse("k".repeat(super::MAX_NAME_KEY_BYTES)).expect("cap is inclusive");
1269 assert_eq!(
1270 NameKey::parse("k".repeat(super::MAX_NAME_KEY_BYTES + 1))
1271 .expect_err("over cap")
1272 .reason(),
1273 "exceeds the maximum name key length of 768 bytes"
1274 );
1275 }
1276
1277 #[test]
1278 fn name_key_serializes_as_string_and_validates_deserialize() {
1279 let name_key = NameKey::parse("report.txt").expect("valid name key");
1280
1281 assert_eq!(
1282 serde_json::to_string(&name_key).expect("serialize name key"),
1283 "\"report.txt\""
1284 );
1285 assert_eq!(
1286 serde_json::from_str::<NameKey>("\"report.txt\"").expect("deserialize name key"),
1287 name_key
1288 );
1289 assert!(serde_json::from_str::<NameKey>("\"a/b\"").is_err());
1290 }
1291}