Skip to main content

ic_storage/
key.rs

1use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
2use std::{fmt, str::FromStr};
3
4const MAX_KEY_BYTES: usize = 128;
5
6///
7/// `StableKey`
8///
9/// Canonical durable logical allocation identity.
10///
11/// A stable key names the logical store, not the current storage backend or
12/// allocation ID. Once committed, the key is permanently bound to its
13/// allocation; changing the key declares a new logical store.
14///
15/// Construction and deserialization enforce the same bounded canonical grammar.
16/// A checked key establishes identity syntax, not allocation or open authority.
17///
18
19#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
20pub struct StableKey(String);
21
22impl<'de> Deserialize<'de> for StableKey {
23    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
24        // Retain the serialized newtype shape before checking its owned text.
25        #[derive(Deserialize)]
26        #[serde(rename = "StableKey")]
27        struct Text(String);
28
29        let Text(value) = Text::deserialize(deserializer)?;
30        validate(&value).map_err(D::Error::custom)?;
31        Ok(Self(value))
32    }
33}
34
35impl StableKey {
36    /// Parse and validate a canonical stable key string.
37    ///
38    /// Keys are bounded lowercase ASCII dot-separated names ending in a
39    /// nonzero `.vN` suffix.
40    ///
41    /// # Errors
42    /// Returns an error when the name exceeds the length limit or violates stable-key syntax.
43    pub fn parse(value: impl AsRef<str>) -> Result<Self, StableKeyError> {
44        let value = value.as_ref();
45        validate(value)?;
46        Ok(Self(value.to_string()))
47    }
48
49    /// Borrow the canonical stable-key string.
50    #[must_use]
51    pub fn as_str(&self) -> &str {
52        &self.0
53    }
54
55    /// Consume the key and return the canonical stable-key string.
56    #[must_use]
57    pub fn into_string(self) -> String {
58        self.0
59    }
60}
61
62impl AsRef<str> for StableKey {
63    fn as_ref(&self) -> &str {
64        self.as_str()
65    }
66}
67
68impl fmt::Display for StableKey {
69    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70        formatter.write_str(self.as_str())
71    }
72}
73
74impl FromStr for StableKey {
75    type Err = StableKeyError;
76
77    fn from_str(value: &str) -> Result<Self, Self::Err> {
78        Self::parse(value)
79    }
80}
81
82///
83/// `StableKeyError`
84///
85/// Stable-key grammar validation failure.
86#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
87#[error("stable key '{stable_key}' is invalid: {reason}")]
88pub struct StableKeyError {
89    /// Rejected stable-key string.
90    pub stable_key: String,
91    /// Stable-key grammar failure.
92    pub reason: &'static str,
93}
94
95pub fn validate(stable_key: &str) -> Result<(), StableKeyError> {
96    if stable_key.is_empty() {
97        return invalid(stable_key, "must not be empty");
98    }
99    if stable_key.len() > MAX_KEY_BYTES {
100        return invalid(stable_key, "must be at most 128 bytes");
101    }
102    // Classify once, retaining the established diagnostic precedence even
103    // when a key contains several different invalid characters.
104    let mut invalid_ascii = false;
105    let mut uppercase = false;
106    let mut whitespace = false;
107    let mut separator = false;
108    for byte in stable_key.bytes() {
109        invalid_ascii |= !byte.is_ascii();
110        uppercase |= byte.is_ascii_uppercase();
111        whitespace |= matches!(byte, b'\t'..=b'\r' | b' ');
112        separator |= matches!(byte, b'/' | b'-');
113    }
114    if invalid_ascii {
115        return invalid(stable_key, "must be ASCII");
116    }
117    if uppercase {
118        return invalid(stable_key, "must be lowercase");
119    }
120    if whitespace {
121        return invalid(stable_key, "must not contain whitespace");
122    }
123    if separator {
124        return invalid(stable_key, "must not contain slashes or hyphens");
125    }
126    if stable_key.starts_with('.') || stable_key.ends_with('.') {
127        return invalid(stable_key, "must not start or end with a dot");
128    }
129
130    let Some(version_index) = stable_key
131        .as_bytes()
132        .windows(2)
133        .rposition(|pair| pair == b".v")
134    else {
135        return invalid(stable_key, "must end with .vN");
136    };
137    let version = &stable_key[version_index + 2..];
138    if version.is_empty()
139        || version.starts_with('0')
140        || !version.bytes().all(|byte| byte.is_ascii_digit())
141    {
142        return invalid(stable_key, "version suffix must be nonzero .vN");
143    }
144
145    let prefix = &stable_key[..version_index];
146    if prefix.is_empty() {
147        return invalid(
148            stable_key,
149            "must contain at least one segment before version",
150        );
151    }
152
153    for segment in prefix.as_bytes().split(|byte| *byte == b'.') {
154        if let Err(reason) = validate_segment(segment) {
155            return invalid(stable_key, reason);
156        }
157    }
158
159    Ok(())
160}
161
162// A namespace plus its synthetic `v1` suffix must fit the same key grammar.
163// Borrow the prefix rather than allocating a temporary key just to discard it.
164pub fn is_key_prefix(value: &str) -> bool {
165    let Some(prefix) = value.strip_suffix('.') else {
166        return false;
167    };
168    value.len() <= MAX_KEY_BYTES - "v1".len()
169        && prefix
170            .as_bytes()
171            .split(|byte| *byte == b'.')
172            .all(|segment| validate_segment(segment).is_ok())
173}
174
175fn validate_segment(segment: &[u8]) -> Result<(), &'static str> {
176    let mut bytes = segment.iter().copied();
177    let Some(first) = bytes.next() else {
178        return Err("must not contain empty segments");
179    };
180    if !first.is_ascii_lowercase() {
181        return Err("segments must start with a lowercase letter");
182    }
183    if !bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') {
184        return Err("segments may contain only lowercase letters, digits, and underscores");
185    }
186    Ok(())
187}
188
189fn invalid<T>(stable_key: &str, reason: &'static str) -> Result<T, StableKeyError> {
190    Err(StableKeyError {
191        stable_key: stable_key.to_string(),
192        reason,
193    })
194}
195
196#[cfg(test)]
197mod tests;