Skip to main content

ic_memory/
key.rs

1use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
2use std::{fmt, str::FromStr};
3
4///
5/// StableKey
6///
7/// Canonical durable logical allocation identity.
8///
9/// A stable key names the logical store, not the current storage backend or
10/// `MemoryManager` ID. Once committed, the key is permanently bound to its
11/// physical allocation slot; changing the key declares a new logical store.
12///
13/// Construction and deserialization enforce the same bounded canonical grammar.
14/// A checked key establishes identity syntax, not allocation or open authority.
15///
16
17#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
18pub struct StableKey(String);
19
20impl<'de> Deserialize<'de> for StableKey {
21    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
22        // Retain the serialized newtype shape before checking its owned text.
23        #[derive(Deserialize)]
24        #[serde(rename = "StableKey")]
25        struct Text(String);
26
27        let Text(value) = Text::deserialize(deserializer)?;
28        validate(&value).map_err(D::Error::custom)?;
29        Ok(Self(value))
30    }
31}
32
33impl StableKey {
34    /// Parse and validate a canonical stable key string.
35    ///
36    /// Keys are bounded lowercase ASCII dot-separated names ending in a
37    /// nonzero `.vN` suffix.
38    pub fn parse(value: impl AsRef<str>) -> Result<Self, StableKeyError> {
39        let value = value.as_ref();
40        validate(value)?;
41        Ok(Self(value.to_string()))
42    }
43
44    /// Borrow the canonical stable-key string.
45    #[must_use]
46    pub fn as_str(&self) -> &str {
47        &self.0
48    }
49
50    /// Consume the key and return the canonical stable-key string.
51    #[must_use]
52    pub fn into_string(self) -> String {
53        self.0
54    }
55}
56
57impl AsRef<str> for StableKey {
58    fn as_ref(&self) -> &str {
59        self.as_str()
60    }
61}
62
63impl fmt::Display for StableKey {
64    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65        formatter.write_str(self.as_str())
66    }
67}
68
69impl FromStr for StableKey {
70    type Err = StableKeyError;
71
72    fn from_str(value: &str) -> Result<Self, Self::Err> {
73        Self::parse(value)
74    }
75}
76
77///
78/// StableKeyError
79///
80/// Stable-key grammar validation failure.
81#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
82#[error("stable key '{stable_key}' is invalid: {reason}")]
83pub struct StableKeyError {
84    /// Rejected stable-key string.
85    pub stable_key: String,
86    /// Stable-key grammar failure.
87    pub reason: &'static str,
88}
89
90pub fn validate(stable_key: &str) -> Result<(), StableKeyError> {
91    if stable_key.is_empty() {
92        return invalid(stable_key, "must not be empty");
93    }
94    if stable_key.len() > 128 {
95        return invalid(stable_key, "must be at most 128 bytes");
96    }
97    if !stable_key.is_ascii() {
98        return invalid(stable_key, "must be ASCII");
99    }
100    if stable_key.bytes().any(|byte| byte.is_ascii_uppercase()) {
101        return invalid(stable_key, "must be lowercase");
102    }
103    if stable_key.contains(char::is_whitespace) {
104        return invalid(stable_key, "must not contain whitespace");
105    }
106    if stable_key.contains('/') || stable_key.contains('-') {
107        return invalid(stable_key, "must not contain slashes or hyphens");
108    }
109    if stable_key.starts_with('.') || stable_key.ends_with('.') {
110        return invalid(stable_key, "must not start or end with a dot");
111    }
112
113    let Some(version_index) = stable_key.rfind(".v") else {
114        return invalid(stable_key, "must end with .vN");
115    };
116    let version = &stable_key[version_index + 2..];
117    if version.is_empty()
118        || version.starts_with('0')
119        || !version.bytes().all(|byte| byte.is_ascii_digit())
120    {
121        return invalid(stable_key, "version suffix must be nonzero .vN");
122    }
123
124    let prefix = &stable_key[..version_index];
125    if prefix.is_empty() {
126        return invalid(
127            stable_key,
128            "must contain at least one segment before version",
129        );
130    }
131
132    for segment in prefix.split('.') {
133        validate_segment(stable_key, segment)?;
134    }
135
136    Ok(())
137}
138
139fn validate_segment(stable_key: &str, segment: &str) -> Result<(), StableKeyError> {
140    let mut bytes = segment.bytes();
141    let Some(first) = bytes.next() else {
142        return invalid(stable_key, "must not contain empty segments");
143    };
144    if !first.is_ascii_lowercase() {
145        return invalid(stable_key, "segments must start with a lowercase letter");
146    }
147    if !bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') {
148        return invalid(
149            stable_key,
150            "segments may contain only lowercase letters, digits, and underscores",
151        );
152    }
153    Ok(())
154}
155
156fn invalid<T>(stable_key: &str, reason: &'static str) -> Result<T, StableKeyError> {
157    Err(StableKeyError {
158        stable_key: stable_key.to_string(),
159        reason,
160    })
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn parse_stores_the_string_it_validates() {
169        struct ChangingKey(std::cell::Cell<bool>);
170
171        impl AsRef<str> for ChangingKey {
172            fn as_ref(&self) -> &str {
173                if self.0.replace(true) {
174                    "INVALID"
175                } else {
176                    "app.rows.v1"
177                }
178            }
179        }
180
181        let key = StableKey::parse(ChangingKey(std::cell::Cell::new(false))).unwrap();
182        assert_eq!(key.as_str(), "app.rows.v1");
183    }
184
185    #[test]
186    fn accepts_canonical_keys() {
187        assert_eq!(
188            StableKey::parse("app.users.primary.v1")
189                .expect("valid key")
190                .as_str(),
191            "app.users.primary.v1"
192        );
193        assert!(StableKey::parse("framework.core.auth_state.v12").is_ok());
194    }
195
196    #[test]
197    fn key_decode_enforces_grammar_and_preserves_current_encoding() {
198        let key = StableKey::parse("app.users.v1").unwrap();
199        let json = serde_json::to_string(&key).unwrap();
200        assert_eq!(json, "\"app.users.v1\"");
201        assert_eq!(serde_json::from_str::<StableKey>(&json).unwrap(), key);
202        let bytes = crate::test_cbor::to_vec(&key).unwrap();
203        assert_eq!(bytes, b"\x6capp.users.v1");
204        assert_eq!(
205            crate::test_cbor::from_slice::<StableKey>(&bytes).unwrap(),
206            key
207        );
208
209        for invalid in [
210            "App.users.v1",
211            "app.users.v0",
212            "app..users.v1",
213            &"x".repeat(129),
214        ] {
215            let json = serde_json::to_string(invalid).unwrap();
216            assert!(serde_json::from_str::<StableKey>(&json).is_err());
217            let bytes = crate::test_cbor::to_vec(&invalid).unwrap();
218            assert!(crate::test_cbor::from_slice::<StableKey>(&bytes).is_err());
219        }
220    }
221
222    #[test]
223    fn rejects_noncanonical_keys() {
224        for key in [
225            "",
226            "App.users.v1",
227            "app.users",
228            "app.users.v0",
229            "app..users.v1",
230            ".app.users.v1",
231            "app.users.v1.",
232            "app-users.v1",
233            "app/users.v1",
234            "app.1users.v1",
235        ] {
236            assert!(StableKey::parse(key).is_err(), "{key} should fail");
237        }
238    }
239}