Skip to main content

aft/commands/semantic_search/
generation_token.rs

1use std::fmt;
2use std::sync::OnceLock;
3
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6static PROCESS_NONCE: OnceLock<[u8; 16]> = OnceLock::new();
7
8/// Process-wide 128-bit CSPRNG process nonce.
9fn get_process_nonce() -> [u8; 16] {
10    *PROCESS_NONCE.get_or_init(|| {
11        let mut nonce = [0u8; 16];
12        getrandom::fill(&mut nonce).expect("failed to generate 128-bit CSPRNG process nonce");
13        nonce
14    })
15}
16
17/// An opaque generation token binding an index snapshot generation and a 128-bit CSPRNG
18/// process nonce into a single opaque string.
19///
20/// Per the spec: equality is the only permitted operation. Substring, split, parse, order,
21/// and range comparisons are strictly forbidden.
22#[derive(Clone, PartialEq, Eq, Hash)]
23pub struct GenerationToken {
24    opaque: String,
25}
26
27impl GenerationToken {
28    /// Construct a new opaque generation token from an index snapshot generation u64
29    /// and the process-wide 128-bit CSPRNG process nonce.
30    pub fn new(snapshot_generation: u64) -> Self {
31        Self::new_with_str(&snapshot_generation.to_string())
32    }
33
34    /// Construct a new opaque generation token from an index snapshot generation string
35    /// and the process-wide 128-bit CSPRNG process nonce.
36    pub fn new_with_str(snapshot_generation: &str) -> Self {
37        Self::new_with_nonce(snapshot_generation, get_process_nonce())
38    }
39
40    /// Construct a token with an explicit 128-bit nonce (useful for deterministic fixtures and testing).
41    pub fn new_with_nonce(snapshot_generation: &str, nonce: [u8; 16]) -> Self {
42        let nonce_hex = u128::from_be_bytes(nonce);
43        let opaque = format!("{snapshot_generation}_{nonce_hex:032x}");
44        Self { opaque }
45    }
46
47    /// Return the opaque token as a string slice for serialization and display.
48    pub fn as_str(&self) -> &str {
49        &self.opaque
50    }
51}
52
53impl fmt::Debug for GenerationToken {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "GenerationToken({})", self.opaque)
56    }
57}
58
59impl fmt::Display for GenerationToken {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(f, "{}", self.opaque)
62    }
63}
64
65impl Serialize for GenerationToken {
66    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
67    where
68        S: Serializer,
69    {
70        serializer.serialize_str(&self.opaque)
71    }
72}
73
74impl<'de> Deserialize<'de> for GenerationToken {
75    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
76    where
77        D: Deserializer<'de>,
78    {
79        let s = String::deserialize(deserializer)?;
80        Ok(Self { opaque: s })
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn equality_and_uniqueness() {
90        let t1 = GenerationToken::new(42);
91        let t2 = GenerationToken::new(42);
92        let t3 = GenerationToken::new(43);
93
94        // Same generation within the same process has same process nonce
95        assert_eq!(t1, t2);
96        // Different generation produces different opaque token
97        assert_ne!(t1, t3);
98
99        // Custom nonces
100        let n1 = [1u8; 16];
101        let n2 = [2u8; 16];
102        let tn1 = GenerationToken::new_with_nonce("42", n1);
103        let tn2 = GenerationToken::new_with_nonce("42", n2);
104        assert_ne!(tn1, tn2);
105    }
106
107    #[test]
108    fn serialization_roundtrip() {
109        let token = GenerationToken::new(100);
110        let json = serde_json::to_string(&token).unwrap();
111        let deserialized: GenerationToken = serde_json::from_str(&json).unwrap();
112        assert_eq!(token, deserialized);
113    }
114}