Skip to main content

ic_memory/
key.rs

1use serde::{Deserialize, Serialize};
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
14#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
15pub struct StableKey(String);
16
17impl StableKey {
18    /// Parse and validate a canonical stable key string.
19    ///
20    /// Keys are bounded lowercase ASCII dot-separated names ending in a
21    /// nonzero `.vN` suffix.
22    pub fn parse(value: impl AsRef<str>) -> Result<Self, StableKeyError> {
23        let value = value.as_ref();
24        validate(value)?;
25        Ok(Self(value.to_string()))
26    }
27
28    /// Borrow the canonical stable-key string.
29    #[must_use]
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33
34    /// Consume the key and return the canonical stable-key string.
35    #[must_use]
36    pub fn into_string(self) -> String {
37        self.0
38    }
39
40    /// Validate constructor invariants after decode.
41    pub fn validate(&self) -> Result<(), StableKeyError> {
42        validate(&self.0)
43    }
44}
45
46impl AsRef<str> for StableKey {
47    fn as_ref(&self) -> &str {
48        self.as_str()
49    }
50}
51
52impl fmt::Display for StableKey {
53    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54        formatter.write_str(self.as_str())
55    }
56}
57
58impl FromStr for StableKey {
59    type Err = StableKeyError;
60
61    fn from_str(value: &str) -> Result<Self, Self::Err> {
62        Self::parse(value)
63    }
64}
65
66///
67/// StableKeyError
68///
69/// Stable-key grammar validation failure.
70#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
71#[error("stable key '{stable_key}' is invalid: {reason}")]
72pub struct StableKeyError {
73    /// Rejected stable-key string.
74    pub stable_key: String,
75    /// Stable-key grammar failure.
76    pub reason: &'static str,
77}
78
79pub fn validate(stable_key: &str) -> Result<(), StableKeyError> {
80    if stable_key.is_empty() {
81        return invalid(stable_key, "must not be empty");
82    }
83    if stable_key.len() > 128 {
84        return invalid(stable_key, "must be at most 128 bytes");
85    }
86    if !stable_key.is_ascii() {
87        return invalid(stable_key, "must be ASCII");
88    }
89    if stable_key.bytes().any(|byte| byte.is_ascii_uppercase()) {
90        return invalid(stable_key, "must be lowercase");
91    }
92    if stable_key.contains(char::is_whitespace) {
93        return invalid(stable_key, "must not contain whitespace");
94    }
95    if stable_key.contains('/') || stable_key.contains('-') {
96        return invalid(stable_key, "must not contain slashes or hyphens");
97    }
98    if stable_key.starts_with('.') || stable_key.ends_with('.') {
99        return invalid(stable_key, "must not start or end with a dot");
100    }
101
102    let Some(version_index) = stable_key.rfind(".v") else {
103        return invalid(stable_key, "must end with .vN");
104    };
105    let version = &stable_key[version_index + 2..];
106    if version.is_empty()
107        || version.starts_with('0')
108        || !version.bytes().all(|byte| byte.is_ascii_digit())
109    {
110        return invalid(stable_key, "version suffix must be nonzero .vN");
111    }
112
113    let prefix = &stable_key[..version_index];
114    if prefix.is_empty() {
115        return invalid(
116            stable_key,
117            "must contain at least one segment before version",
118        );
119    }
120
121    for segment in prefix.split('.') {
122        validate_segment(stable_key, segment)?;
123    }
124
125    Ok(())
126}
127
128fn validate_segment(stable_key: &str, segment: &str) -> Result<(), StableKeyError> {
129    let mut bytes = segment.bytes();
130    let Some(first) = bytes.next() else {
131        return invalid(stable_key, "must not contain empty segments");
132    };
133    if !first.is_ascii_lowercase() {
134        return invalid(stable_key, "segments must start with a lowercase letter");
135    }
136    if !bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') {
137        return invalid(
138            stable_key,
139            "segments may contain only lowercase letters, digits, and underscores",
140        );
141    }
142    Ok(())
143}
144
145fn invalid<T>(stable_key: &str, reason: &'static str) -> Result<T, StableKeyError> {
146    Err(StableKeyError {
147        stable_key: stable_key.to_string(),
148        reason,
149    })
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn parse_stores_the_string_it_validates() {
158        struct ChangingKey(std::cell::Cell<bool>);
159
160        impl AsRef<str> for ChangingKey {
161            fn as_ref(&self) -> &str {
162                if self.0.replace(true) {
163                    "INVALID"
164                } else {
165                    "app.rows.v1"
166                }
167            }
168        }
169
170        let key = StableKey::parse(ChangingKey(std::cell::Cell::new(false))).unwrap();
171        assert_eq!(key.as_str(), "app.rows.v1");
172        key.validate().unwrap();
173    }
174
175    #[test]
176    fn accepts_canonical_keys() {
177        assert_eq!(
178            StableKey::parse("app.users.primary.v1")
179                .expect("valid key")
180                .as_str(),
181            "app.users.primary.v1"
182        );
183        assert!(StableKey::parse("framework.core.auth_state.v12").is_ok());
184    }
185
186    #[test]
187    fn rejects_noncanonical_keys() {
188        for key in [
189            "",
190            "App.users.v1",
191            "app.users",
192            "app.users.v0",
193            "app..users.v1",
194            ".app.users.v1",
195            "app.users.v1.",
196            "app-users.v1",
197            "app/users.v1",
198            "app.1users.v1",
199        ] {
200            assert!(StableKey::parse(key).is_err(), "{key} should fail");
201        }
202    }
203}