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