Skip to main content

ic_memory/
key.rs

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