Skip to main content

lodestone_types/
identity.rs

1use std::error::Error;
2use std::fmt::{self, Display, Formatter};
3use std::num::{NonZeroU64, NonZeroU128};
4use std::str::FromStr;
5
6use serde::de;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9macro_rules! opaque_u128_id {
10    ($name:ident, $description:literal) => {
11        #[doc = $description]
12        #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13        pub struct $name(NonZeroU128);
14
15        impl $name {
16            pub fn new(value: u128) -> Result<Self, IdentityError> {
17                NonZeroU128::new(value).map(Self).ok_or(IdentityError::Zero {
18                    kind: stringify!($name),
19                })
20            }
21
22            pub fn from_be_bytes(bytes: [u8; 16]) -> Result<Self, IdentityError> {
23                Self::new(u128::from_be_bytes(bytes))
24            }
25
26            #[must_use]
27            pub const fn get(self) -> u128 {
28                self.0.get()
29            }
30
31            #[must_use]
32            pub const fn to_be_bytes(self) -> [u8; 16] {
33                self.get().to_be_bytes()
34            }
35
36            #[must_use]
37            pub fn checked_next(self) -> Option<Self> {
38                self.get().checked_add(1).and_then(NonZeroU128::new).map(Self)
39            }
40        }
41
42        impl Display for $name {
43            fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
44                write!(formatter, "{:032x}", self.get())
45            }
46        }
47
48        impl FromStr for $name {
49            type Err = IdentityError;
50
51            fn from_str(value: &str) -> Result<Self, Self::Err> {
52                parse_hex_u128(stringify!($name), value).and_then(Self::new)
53            }
54        }
55
56        impl Serialize for $name {
57            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
58            where
59                S: Serializer,
60            {
61                serializer.collect_str(self)
62            }
63        }
64
65        impl<'de> Deserialize<'de> for $name {
66            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
67            where
68                D: Deserializer<'de>,
69            {
70                let value = String::deserialize(deserializer)?;
71                value.parse().map_err(de::Error::custom)
72            }
73        }
74    };
75}
76
77opaque_u128_id!(
78    CollectionId,
79    "An immutable nonzero collection namespace encoded as 32 lower-case hexadecimal characters."
80);
81opaque_u128_id!(
82    IndexId,
83    "An immutable nonzero index-incarnation identity encoded as 32 lower-case hexadecimal characters."
84);
85opaque_u128_id!(
86    ObjectIncarnation,
87    "A never-reused nonzero physical-object identity within one backend authority."
88);
89opaque_u128_id!(
90    ProtectionSnapshotId,
91    "A never-reused semantic protection-snapshot identity derived from a burned control-authority allocator value."
92);
93opaque_u128_id!(
94    RecoveryAttemptId,
95    "A stable nonzero identity reused by every retry of one offline total-voter recovery attempt."
96);
97/// Stable object-namespace identity derived once from the bootstrap cluster
98/// identity and persisted unchanged across recovery eras.
99#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
100pub struct ClusterStorageId([u8; 16]);
101
102impl ClusterStorageId {
103    const DERIVATION_DOMAIN: &'static [u8] = b"lodestone:cluster-storage-id:v1\0";
104
105    pub fn derive_from_genesis(cluster_identity: ClusterIdentity) -> Result<Self, IdentityError> {
106        if cluster_identity.recovery_generation() != RecoveryGeneration::genesis() {
107            return Err(IdentityError::ClusterStorageIdNonGenesis(
108                cluster_identity.recovery_generation().get(),
109            ));
110        }
111        let mut hasher = blake3::Hasher::new();
112        hasher.update(Self::DERIVATION_DOMAIN);
113        hasher.update(&cluster_identity.recovery_era().to_be_bytes());
114        let digest = hasher.finalize();
115        let mut bytes = [0_u8; 16];
116        bytes.copy_from_slice(&digest.as_bytes()[..16]);
117        Ok(Self::from_be_bytes(bytes))
118    }
119
120    /// Decodes a persisted stable storage identity.
121    #[must_use]
122    pub const fn from_be_bytes(bytes: [u8; 16]) -> Self {
123        Self(bytes)
124    }
125
126    #[must_use]
127    pub const fn to_be_bytes(self) -> [u8; 16] {
128        self.0
129    }
130}
131
132impl Display for ClusterStorageId {
133    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
134        write!(formatter, "{:032x}", u128::from_be_bytes(self.0))
135    }
136}
137
138impl FromStr for ClusterStorageId {
139    type Err = IdentityError;
140
141    fn from_str(value: &str) -> Result<Self, Self::Err> {
142        let value = parse_hex_u128("ClusterStorageId", value)?;
143        Ok(Self::from_be_bytes(value.to_be_bytes()))
144    }
145}
146
147impl Serialize for ClusterStorageId {
148    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
149    where
150        S: Serializer,
151    {
152        serializer.collect_str(self)
153    }
154}
155
156impl<'de> Deserialize<'de> for ClusterStorageId {
157    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
158    where
159        D: Deserializer<'de>,
160    {
161        String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
162    }
163}
164
165/// A checked, nonzero process incarnation local to one durable `NodeId`.
166#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
167pub struct NodeIncarnation(NonZeroU64);
168
169impl NodeIncarnation {
170    pub fn new(value: u64) -> Result<Self, IdentityError> {
171        NonZeroU64::new(value).map(Self).ok_or(IdentityError::Zero {
172            kind: "NodeIncarnation",
173        })
174    }
175
176    #[must_use]
177    pub const fn get(self) -> u64 {
178        self.0.get()
179    }
180
181    #[must_use]
182    pub fn checked_next(self) -> Option<Self> {
183        self.get().checked_add(1).and_then(NonZeroU64::new).map(Self)
184    }
185}
186
187impl Display for NodeIncarnation {
188    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
189        Display::fmt(&self.get(), formatter)
190    }
191}
192
193impl Serialize for NodeIncarnation {
194    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
195    where
196        S: Serializer,
197    {
198        serializer.serialize_u64(self.get())
199    }
200}
201
202impl<'de> Deserialize<'de> for NodeIncarnation {
203    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
204    where
205        D: Deserializer<'de>,
206    {
207        Self::new(u64::deserialize(deserializer)?).map_err(de::Error::custom)
208    }
209}
210
211/// An opaque cluster fencing identity advanced only by offline total-voter
212/// recovery.
213///
214/// Unlike allocator-backed identities, this type intentionally exposes no
215/// numeric value or successor operation. Recovery-era order is meaningful
216/// only inside the bounded offline lineage protocol.
217#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
218pub struct RecoveryEra(NonZeroU128);
219
220impl RecoveryEra {
221    pub fn new(value: u128) -> Result<Self, IdentityError> {
222        NonZeroU128::new(value)
223            .map(Self)
224            .ok_or(IdentityError::Zero { kind: "RecoveryEra" })
225    }
226
227    pub fn from_be_bytes(bytes: [u8; 16]) -> Result<Self, IdentityError> {
228        Self::new(u128::from_be_bytes(bytes))
229    }
230
231    #[must_use]
232    pub const fn to_be_bytes(self) -> [u8; 16] {
233        self.0.get().to_be_bytes()
234    }
235}
236
237impl Display for RecoveryEra {
238    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
239        write!(formatter, "{:032x}", self.0.get())
240    }
241}
242
243impl FromStr for RecoveryEra {
244    type Err = IdentityError;
245
246    fn from_str(value: &str) -> Result<Self, Self::Err> {
247        parse_hex_u128("RecoveryEra", value).and_then(Self::new)
248    }
249}
250
251impl Serialize for RecoveryEra {
252    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
253    where
254        S: Serializer,
255    {
256        serializer.collect_str(self)
257    }
258}
259
260impl<'de> Deserialize<'de> for RecoveryEra {
261    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
262    where
263        D: Deserializer<'de>,
264    {
265        String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
266    }
267}
268
269fn parse_hex_u128(kind: &'static str, value: &str) -> Result<u128, IdentityError> {
270    if value.len() != 32 {
271        return Err(IdentityError::HexLength {
272            kind,
273            actual: value.len(),
274        });
275    }
276    if !value
277        .bytes()
278        .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
279    {
280        return Err(IdentityError::LowerHex { kind });
281    }
282    u128::from_str_radix(value, 16).map_err(|_| IdentityError::LowerHex { kind })
283}
284
285/// A bounded human-facing index name.
286#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
287#[serde(transparent)]
288pub struct IndexName(String);
289
290impl IndexName {
291    pub fn new(value: impl Into<String>) -> Result<Self, IdentityError> {
292        let value = value.into();
293        let length = value.len();
294        if !(1..=255).contains(&length) {
295            return Err(IdentityError::IndexNameLength(length));
296        }
297        if value.bytes().any(|byte| byte <= 0x1f || byte == 0x7f) {
298            return Err(IdentityError::IndexNameControl);
299        }
300        Ok(Self(value))
301    }
302
303    #[must_use]
304    pub fn as_str(&self) -> &str {
305        &self.0
306    }
307
308    #[must_use]
309    pub fn into_inner(self) -> String {
310        self.0
311    }
312
313    /// Encodes the bounded binary form as a big-endian u16 length followed by
314    /// the exact UTF-8 bytes.
315    #[must_use]
316    pub fn to_binary(&self) -> Vec<u8> {
317        let length = self.0.len() as u16;
318        let mut bytes = Vec::with_capacity(2 + usize::from(length));
319        bytes.extend_from_slice(&length.to_be_bytes());
320        bytes.extend_from_slice(self.0.as_bytes());
321        bytes
322    }
323
324    /// Decodes one exact bounded binary value, refusing trailing bytes.
325    pub fn from_binary(bytes: &[u8]) -> Result<Self, IdentityError> {
326        let Some(length_bytes) = bytes.get(..2) else {
327            return Err(IdentityError::BinaryLength {
328                kind: "IndexName",
329                expected: 2,
330                actual: bytes.len(),
331            });
332        };
333        let length = usize::from(u16::from_be_bytes([length_bytes[0], length_bytes[1]]));
334        if bytes.len() != 2 + length {
335            return Err(IdentityError::BinaryLength {
336                kind: "IndexName",
337                expected: 2 + length,
338                actual: bytes.len(),
339            });
340        }
341        let value = std::str::from_utf8(&bytes[2..]).map_err(|_| IdentityError::IndexNameUtf8)?;
342        Self::new(value)
343    }
344}
345
346impl Display for IndexName {
347    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
348        formatter.write_str(self.as_str())
349    }
350}
351
352impl FromStr for IndexName {
353    type Err = IdentityError;
354
355    fn from_str(value: &str) -> Result<Self, Self::Err> {
356        Self::new(value)
357    }
358}
359
360impl<'de> Deserialize<'de> for IndexName {
361    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
362    where
363        D: Deserializer<'de>,
364    {
365        String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
366    }
367}
368
369/// Stable identity for one canonical object-storage namespace.
370#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
371#[serde(transparent)]
372pub struct BackendAuthorityId(String);
373
374impl BackendAuthorityId {
375    pub fn new(value: impl Into<String>) -> Result<Self, IdentityError> {
376        let value = value.into();
377        let bytes = value.as_bytes();
378        if !(1..=64).contains(&bytes.len())
379            || !bytes.first().is_some_and(u8::is_ascii_lowercase)
380            || !bytes
381                .iter()
382                .skip(1)
383                .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-'))
384        {
385            return Err(IdentityError::BackendAuthority(value));
386        }
387        Ok(Self(value))
388    }
389
390    #[must_use]
391    pub fn as_str(&self) -> &str {
392        &self.0
393    }
394
395    /// Encodes the bounded binary form as a one-byte length followed by the
396    /// exact ASCII identity.
397    #[must_use]
398    pub fn to_binary(&self) -> Vec<u8> {
399        let length = self.0.len() as u8;
400        let mut bytes = Vec::with_capacity(1 + usize::from(length));
401        bytes.push(length);
402        bytes.extend_from_slice(self.0.as_bytes());
403        bytes
404    }
405
406    /// Decodes one exact bounded binary value, refusing trailing bytes.
407    pub fn from_binary(bytes: &[u8]) -> Result<Self, IdentityError> {
408        let Some(length) = bytes.first().copied() else {
409            return Err(IdentityError::BinaryLength {
410                kind: "BackendAuthorityId",
411                expected: 1,
412                actual: 0,
413            });
414        };
415        let expected = 1 + usize::from(length);
416        if bytes.len() != expected {
417            return Err(IdentityError::BinaryLength {
418                kind: "BackendAuthorityId",
419                expected,
420                actual: bytes.len(),
421            });
422        }
423        let value = std::str::from_utf8(&bytes[1..]).map_err(|_| IdentityError::BackendAuthorityUtf8)?;
424        Self::new(value)
425    }
426}
427
428impl Display for BackendAuthorityId {
429    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
430        formatter.write_str(self.as_str())
431    }
432}
433
434impl FromStr for BackendAuthorityId {
435    type Err = IdentityError;
436
437    fn from_str(value: &str) -> Result<Self, Self::Err> {
438        Self::new(value)
439    }
440}
441
442impl<'de> Deserialize<'de> for BackendAuthorityId {
443    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
444    where
445        D: Deserializer<'de>,
446    {
447        String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
448    }
449}
450
451/// The immutable catalog ownership identity of one index incarnation.
452#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
453#[serde(deny_unknown_fields)]
454pub struct CollectionIndexRef {
455    collection_id: CollectionId,
456    index_id: IndexId,
457}
458
459impl CollectionIndexRef {
460    #[must_use]
461    pub const fn new(collection_id: CollectionId, index_id: IndexId) -> Self {
462        Self {
463            collection_id,
464            index_id,
465        }
466    }
467
468    #[must_use]
469    pub const fn collection_id(self) -> CollectionId {
470        self.collection_id
471    }
472
473    #[must_use]
474    pub const fn index_id(self) -> IndexId {
475        self.index_id
476    }
477}
478
479impl Display for CollectionIndexRef {
480    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
481        write!(formatter, "{}/{}", self.collection_id, self.index_id)
482    }
483}
484
485/// A human address that must be resolved once through the catalog.
486#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
487#[serde(deny_unknown_fields)]
488pub struct IndexAddress {
489    collection_id: CollectionId,
490    index_name: IndexName,
491}
492
493impl IndexAddress {
494    #[must_use]
495    pub const fn new(collection_id: CollectionId, index_name: IndexName) -> Self {
496        Self {
497            collection_id,
498            index_name,
499        }
500    }
501
502    #[must_use]
503    pub const fn collection_id(&self) -> CollectionId {
504        self.collection_id
505    }
506
507    #[must_use]
508    pub fn index_name(&self) -> &IndexName {
509        &self.index_name
510    }
511}
512
513/// A shard identity that is meaningful at production boundaries.
514#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
515#[serde(deny_unknown_fields)]
516pub struct ShardKey {
517    collection_index_ref: CollectionIndexRef,
518    shard_id: crate::ShardId,
519}
520
521impl ShardKey {
522    #[must_use]
523    pub const fn new(collection_index_ref: CollectionIndexRef, shard_id: crate::ShardId) -> Self {
524        Self {
525            collection_index_ref,
526            shard_id,
527        }
528    }
529
530    #[must_use]
531    pub const fn collection_index_ref(self) -> CollectionIndexRef {
532        self.collection_index_ref
533    }
534
535    #[must_use]
536    pub const fn shard_id(self) -> crate::ShardId {
537        self.shard_id
538    }
539}
540
541/// The immutable WAL/data authority pair copied into an index catalog record.
542#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
543#[serde(deny_unknown_fields)]
544pub struct BackendAuthorityPair {
545    wal: BackendAuthorityId,
546    data: BackendAuthorityId,
547}
548
549impl BackendAuthorityPair {
550    #[must_use]
551    pub const fn new(wal: BackendAuthorityId, data: BackendAuthorityId) -> Self {
552        Self { wal, data }
553    }
554
555    #[must_use]
556    pub fn wal(&self) -> &BackendAuthorityId {
557        &self.wal
558    }
559
560    #[must_use]
561    pub fn data(&self) -> &BackendAuthorityId {
562        &self.data
563    }
564}
565
566/// Checked public-generation component advanced once per offline recovery.
567#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
568pub struct RecoveryGeneration(u32);
569
570impl RecoveryGeneration {
571    pub const MAX: u32 = 0x7fff_ffff;
572
573    pub fn new(value: u32) -> Result<Self, IdentityError> {
574        if value <= Self::MAX {
575            Ok(Self(value))
576        } else {
577            Err(IdentityError::RecoveryGeneration(value))
578        }
579    }
580
581    #[must_use]
582    pub const fn genesis() -> Self {
583        Self(0)
584    }
585
586    #[must_use]
587    pub const fn get(self) -> u32 {
588        self.0
589    }
590
591    #[must_use]
592    pub fn checked_next(self) -> Option<Self> {
593        self.0.checked_add(1).filter(|value| *value <= Self::MAX).map(Self)
594    }
595}
596
597impl Serialize for RecoveryGeneration {
598    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
599    where
600        S: Serializer,
601    {
602        serializer.serialize_u32(self.get())
603    }
604}
605
606impl<'de> Deserialize<'de> for RecoveryGeneration {
607    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
608    where
609        D: Deserializer<'de>,
610    {
611        Self::new(u32::deserialize(deserializer)?).map_err(de::Error::custom)
612    }
613}
614
615impl Display for RecoveryGeneration {
616    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
617        Display::fmt(&self.get(), formatter)
618    }
619}
620
621/// Recovery-stable positive public optimistic-concurrency term.
622#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
623pub struct PrimaryTerm(u64);
624
625impl PrimaryTerm {
626    pub fn new(value: u64) -> Result<Self, IdentityError> {
627        if value == 0 || value > i64::MAX as u64 || value as u32 == 0 {
628            Err(IdentityError::PrimaryTerm(value))
629        } else {
630            Ok(Self(value))
631        }
632    }
633
634    pub fn from_parts(recovery_generation: RecoveryGeneration, epoch: crate::Epoch) -> Result<Self, IdentityError> {
635        if epoch.get() == 0 {
636            return Err(IdentityError::ZeroEpoch);
637        }
638        let value = (u64::from(recovery_generation.get()) << 32) | u64::from(epoch.get());
639        Self::new(value)
640    }
641
642    /// Constructs the term for a freshly bootstrapped writer before any total
643    /// recovery generation change.
644    #[must_use]
645    pub const fn from_genesis_epoch(epoch: crate::Epoch) -> Self {
646        Self(epoch.get() as u64)
647    }
648
649    #[must_use]
650    pub const fn get(self) -> u64 {
651        self.0
652    }
653
654    #[must_use]
655    pub const fn epoch_component(self) -> crate::Epoch {
656        crate::Epoch::new(self.0 as u32)
657    }
658
659    #[must_use]
660    pub const fn recovery_generation_component(self) -> RecoveryGeneration {
661        RecoveryGeneration((self.0 >> 32) as u32)
662    }
663}
664
665impl Serialize for PrimaryTerm {
666    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
667    where
668        S: Serializer,
669    {
670        serializer.serialize_u64(self.get())
671    }
672}
673
674impl<'de> Deserialize<'de> for PrimaryTerm {
675    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
676    where
677        D: Deserializer<'de>,
678    {
679        Self::new(u64::deserialize(deserializer)?).map_err(de::Error::custom)
680    }
681}
682
683impl Display for PrimaryTerm {
684    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
685        Display::fmt(&self.get(), formatter)
686    }
687}
688
689/// Complete internal writer/task fence. Public primary terms never replace it.
690#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
691#[serde(deny_unknown_fields)]
692pub struct FenceToken {
693    recovery_era: RecoveryEra,
694    epoch: crate::Epoch,
695}
696
697impl FenceToken {
698    pub fn new(recovery_era: RecoveryEra, epoch: crate::Epoch) -> Result<Self, IdentityError> {
699        if epoch.get() == 0 {
700            return Err(IdentityError::ZeroEpoch);
701        }
702        Ok(Self { recovery_era, epoch })
703    }
704
705    #[must_use]
706    pub const fn recovery_era(self) -> RecoveryEra {
707        self.recovery_era
708    }
709
710    #[must_use]
711    pub const fn epoch(self) -> crate::Epoch {
712        self.epoch
713    }
714
715    /// Encodes the complete fence as 16-byte recovery era plus 4-byte
716    /// big-endian epoch.
717    #[must_use]
718    pub fn to_binary(self) -> [u8; 20] {
719        let mut bytes = [0_u8; 20];
720        bytes[..16].copy_from_slice(&self.recovery_era.to_be_bytes());
721        bytes[16..].copy_from_slice(&self.epoch.get().to_be_bytes());
722        bytes
723    }
724
725    /// Decodes and validates one exact complete-fence binary value.
726    pub fn from_binary(bytes: &[u8]) -> Result<Self, IdentityError> {
727        if bytes.len() != 20 {
728            return Err(IdentityError::BinaryLength {
729                kind: "FenceToken",
730                expected: 20,
731                actual: bytes.len(),
732            });
733        }
734        let mut era_bytes = [0_u8; 16];
735        era_bytes.copy_from_slice(&bytes[..16]);
736        let recovery_era = RecoveryEra::from_be_bytes(era_bytes)?;
737        let epoch = crate::Epoch::new(u32::from_be_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]));
738        Self::new(recovery_era, epoch)
739    }
740}
741
742impl<'de> Deserialize<'de> for FenceToken {
743    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
744    where
745        D: Deserializer<'de>,
746    {
747        #[derive(Deserialize)]
748        #[serde(deny_unknown_fields)]
749        struct Wire {
750            recovery_era: RecoveryEra,
751            epoch: crate::Epoch,
752        }
753
754        let wire = Wire::deserialize(deserializer)?;
755        Self::new(wire.recovery_era, wire.epoch).map_err(de::Error::custom)
756    }
757}
758
759/// Exact opaque and numeric recovery identity pair stored in snapshots and markers.
760#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
761#[serde(deny_unknown_fields)]
762pub struct ClusterIdentity {
763    recovery_era: RecoveryEra,
764    recovery_generation: RecoveryGeneration,
765}
766
767impl ClusterIdentity {
768    #[must_use]
769    pub const fn new(recovery_era: RecoveryEra, recovery_generation: RecoveryGeneration) -> Self {
770        Self {
771            recovery_era,
772            recovery_generation,
773        }
774    }
775
776    #[must_use]
777    pub const fn recovery_era(self) -> RecoveryEra {
778        self.recovery_era
779    }
780
781    #[must_use]
782    pub const fn recovery_generation(self) -> RecoveryGeneration {
783        self.recovery_generation
784    }
785}
786
787/// Validation failure for P5 durable identities.
788#[derive(Clone, Debug, Eq, PartialEq)]
789pub enum IdentityError {
790    Zero {
791        kind: &'static str,
792    },
793    HexLength {
794        kind: &'static str,
795        actual: usize,
796    },
797    LowerHex {
798        kind: &'static str,
799    },
800    BinaryLength {
801        kind: &'static str,
802        expected: usize,
803        actual: usize,
804    },
805    IndexNameLength(usize),
806    IndexNameControl,
807    IndexNameUtf8,
808    BackendAuthority(String),
809    BackendAuthorityUtf8,
810    ClusterStorageIdNonGenesis(u32),
811    RecoveryGeneration(u32),
812    ZeroEpoch,
813    PrimaryTerm(u64),
814}
815
816impl Display for IdentityError {
817    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
818        match self {
819            Self::Zero { kind } => write!(formatter, "{kind} must be nonzero"),
820            Self::HexLength { kind, actual } => {
821                write!(
822                    formatter,
823                    "{kind} must contain exactly 32 lower-case hexadecimal bytes; got {actual}"
824                )
825            }
826            Self::LowerHex { kind } => {
827                write!(
828                    formatter,
829                    "{kind} must contain exactly 32 lower-case hexadecimal characters"
830                )
831            }
832            Self::BinaryLength { kind, expected, actual } => write!(
833                formatter,
834                "{kind} binary encoding requires exactly {expected} bytes; got {actual}"
835            ),
836            Self::IndexNameLength(length) => {
837                write!(formatter, "index name is {length} bytes; expected 1..=255")
838            }
839            Self::IndexNameControl => formatter.write_str("index name contains an ASCII control character"),
840            Self::IndexNameUtf8 => formatter.write_str("index name binary encoding is not valid UTF-8"),
841            Self::BackendAuthority(value) => write!(
842                formatter,
843                "backend authority '{value}' must match [a-z][a-z0-9_-]{{0,63}}"
844            ),
845            Self::BackendAuthorityUtf8 => formatter.write_str("backend authority binary encoding is not valid UTF-8"),
846            Self::ClusterStorageIdNonGenesis(generation) => write!(
847                formatter,
848                "cluster storage identity can be derived only from recovery generation zero; got {generation}"
849            ),
850            Self::RecoveryGeneration(value) => {
851                write!(
852                    formatter,
853                    "recovery generation {value} exceeds the checked 31-bit domain"
854                )
855            }
856            Self::ZeroEpoch => formatter.write_str("a fenced epoch or primary term epoch component must be nonzero"),
857            Self::PrimaryTerm(value) => {
858                write!(
859                    formatter,
860                    "primary term {value} must be in 1..=i64::MAX with a nonzero low epoch component"
861                )
862            }
863        }
864    }
865}
866
867impl Error for IdentityError {}
868
869#[cfg(test)]
870mod tests {
871    use std::error::Error;
872
873    use super::{
874        BackendAuthorityId, ClusterIdentity, ClusterStorageId, CollectionId, CollectionIndexRef, FenceToken,
875        IndexAddress, IndexId, IndexName, NodeIncarnation, ObjectIncarnation, PrimaryTerm, ProtectionSnapshotId,
876        RecoveryAttemptId, RecoveryEra, RecoveryGeneration, ShardKey,
877    };
878    use crate::{Epoch, ShardId};
879
880    #[test]
881    fn opaque_ids_are_nonzero_fixed_lower_hex_and_big_endian() {
882        let collection = CollectionId::new(0x102).ok();
883        assert_eq!(
884            collection.and_then(|value| serde_json::to_string(&value).ok()),
885            Some("\"00000000000000000000000000000102\"".to_owned())
886        );
887        assert_eq!(
888            collection.map(CollectionId::to_be_bytes),
889            Some(0x102_u128.to_be_bytes())
890        );
891        assert!(CollectionId::new(0).is_err());
892        assert!(serde_json::from_str::<CollectionId>("\"00000000000000000000000000000000\"").is_err());
893        assert!(serde_json::from_str::<CollectionId>("\"000000000000000000000000000000AF\"").is_err());
894        assert!(serde_json::from_str::<CollectionId>("1").is_err());
895    }
896
897    #[test]
898    fn node_incarnation_is_nonzero_monotonic_and_numeric_on_the_wire() -> Result<(), Box<dyn Error>> {
899        let incarnation = NodeIncarnation::new(7)?;
900        assert_eq!(incarnation.get(), 7);
901        assert_eq!(incarnation.checked_next().map(|value| value.get()), Some(8));
902        assert_eq!(serde_json::to_string(&incarnation)?, "7");
903        assert_eq!(serde_json::from_str::<NodeIncarnation>("7")?, incarnation);
904        assert!(NodeIncarnation::new(0).is_err());
905        assert!(serde_json::from_str::<NodeIncarnation>("0").is_err());
906        Ok(())
907    }
908
909    #[test]
910    fn recovery_attempt_identity_uses_the_opaque_hex_contract() -> Result<(), Box<dyn Error>> {
911        let identity = RecoveryAttemptId::new(9)?;
912        assert_eq!(identity.to_string(), "00000000000000000000000000000009");
913        assert_eq!(identity.to_string().parse::<RecoveryAttemptId>()?, identity);
914        let protection = ProtectionSnapshotId::new(11)?;
915        assert_eq!(protection.to_string(), "0000000000000000000000000000000b");
916        assert_eq!(protection.to_string().parse::<ProtectionSnapshotId>()?, protection);
917        assert!(ProtectionSnapshotId::new(0).is_err());
918        Ok(())
919    }
920
921    #[test]
922    fn names_and_backend_authorities_enforce_catalog_bounds() {
923        let name = IndexName::new("products");
924        assert!(name.is_ok());
925        if let Ok(value) = &name {
926            assert_eq!(IndexName::from_binary(&value.to_binary()).as_ref(), Ok(value));
927        }
928        assert!(IndexName::new("").is_err());
929        assert!(IndexName::new("bad\u{7f}name").is_err());
930        assert!(IndexName::new("x".repeat(256)).is_err());
931        let authority = BackendAuthorityId::new("standard_aps1");
932        if let Ok(value) = &authority {
933            assert_eq!(BackendAuthorityId::from_binary(&value.to_binary()).as_ref(), Ok(value));
934        }
935        assert!(BackendAuthorityId::new("Express").is_err());
936        assert!(BackendAuthorityId::new("-leading").is_err());
937        assert!(BackendAuthorityId::from_binary(&[3, b'a', b'b']).is_err());
938    }
939
940    #[test]
941    fn scopes_and_addresses_round_trip_without_unknown_fields() -> Result<(), Box<dyn Error>> {
942        let collection = CollectionId::new(1)?;
943        let index = IndexId::new(2)?;
944        let name = IndexName::new("events")?;
945        let scope = CollectionIndexRef::new(collection, index);
946        let address = IndexAddress::new(collection, name);
947        let shard = ShardKey::new(scope, ShardId::new(7));
948        let encoded_address = serde_json::to_vec(&address)?;
949        assert_eq!(serde_json::from_slice::<IndexAddress>(&encoded_address)?, address);
950        let encoded_shard = serde_json::to_vec(&shard)?;
951        assert_eq!(serde_json::from_slice::<ShardKey>(&encoded_shard)?, shard);
952        assert!(serde_json::from_str::<CollectionIndexRef>(
953            r#"{"collection_id":"00000000000000000000000000000001","index_id":"00000000000000000000000000000002","extra":1}"#
954        )
955        .is_err());
956        Ok(())
957    }
958
959    #[test]
960    fn recovery_generation_projects_a_positive_signed_primary_term() -> Result<(), Box<dyn Error>> {
961        let era = RecoveryEra::new(9)?;
962        let generation = RecoveryGeneration::new(1)?;
963        let term = PrimaryTerm::from_parts(generation, Epoch::new(7))?;
964        assert_eq!(term.get(), (1_u64 << 32) | 7);
965        assert_eq!(term.recovery_generation_component(), generation);
966        assert_eq!(term.epoch_component(), Epoch::new(7));
967        assert!(PrimaryTerm::from_parts(generation, Epoch::new(0)).is_err());
968        assert!(PrimaryTerm::new(1_u64 << 32).is_err());
969        assert!(RecoveryGeneration::new(RecoveryGeneration::MAX + 1).is_err());
970        assert!(
971            RecoveryGeneration::new(RecoveryGeneration::MAX)
972                .ok()
973                .and_then(RecoveryGeneration::checked_next)
974                .is_none()
975        );
976        let fence = FenceToken::new(era, Epoch::new(7))?;
977        assert_eq!(FenceToken::from_binary(&fence.to_binary())?, fence);
978        assert!(FenceToken::from_binary(&[0; 20]).is_err());
979        assert!(
980            serde_json::from_str::<FenceToken>(r#"{"recovery_era":"00000000000000000000000000000009","epoch":0}"#)
981                .is_err()
982        );
983        assert_eq!(ClusterIdentity::new(era, generation).recovery_generation(), generation);
984        Ok(())
985    }
986
987    #[test]
988    fn cluster_storage_identity_is_genesis_derived_and_stable() -> Result<(), Box<dyn Error>> {
989        let genesis = ClusterIdentity::new(RecoveryEra::new(9)?, RecoveryGeneration::genesis());
990        let storage_id = ClusterStorageId::derive_from_genesis(genesis)?;
991        assert_eq!(storage_id.to_string().len(), 32);
992        assert_eq!(storage_id.to_string().parse::<ClusterStorageId>()?, storage_id);
993        assert_eq!(ClusterStorageId::from_be_bytes(storage_id.to_be_bytes()), storage_id);
994        assert_eq!(
995            serde_json::from_str::<ClusterStorageId>(&serde_json::to_string(&storage_id)?)?,
996            storage_id
997        );
998        assert_eq!(
999            ClusterStorageId::derive_from_genesis(genesis)?,
1000            ClusterStorageId::derive_from_genesis(genesis)?
1001        );
1002        let successor = ClusterIdentity::new(RecoveryEra::new(10)?, RecoveryGeneration::new(1)?);
1003        assert_eq!(
1004            ClusterStorageId::derive_from_genesis(successor),
1005            Err(super::IdentityError::ClusterStorageIdNonGenesis(1))
1006        );
1007        assert_eq!(ClusterStorageId::from_be_bytes([0; 16]).to_be_bytes(), [0; 16]);
1008        Ok(())
1009    }
1010
1011    #[test]
1012    fn every_nonzero_physical_identity_uses_the_same_codec_contract() -> Result<(), Box<dyn Error>> {
1013        let index = IndexId::new(u128::MAX)?;
1014        let object = ObjectIncarnation::new(u128::MAX)?;
1015        assert_eq!(index.to_string(), object.to_string());
1016        assert!(ObjectIncarnation::from_be_bytes([0; 16]).is_err());
1017        Ok(())
1018    }
1019}