Skip to main content

miden_protocol/utils/
strings.rs

1use alloc::fmt;
2use alloc::string::String;
3
4use crate::Felt;
5use crate::errors::ShortCapitalStringError;
6
7/// A short string of uppercase ASCII (and optionally underscores) encoded into a [`Felt`] with a
8/// configurable alphabet.
9///
10/// Use [`Self::from_ascii_uppercase`] or [`Self::from_ascii_uppercase_and_underscore`] to construct
11/// a validated value (same rules as [`crate::asset::TokenSymbol`] and
12/// [`crate::account::RoleSymbol`]).
13///
14/// The text is stored as a [`String`] and can be converted to a [`Felt`] encoding via
15/// [`as_element()`](Self::as_element), and decoded back via
16/// [`try_from_encoded_felt()`](Self::try_from_encoded_felt).
17#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
18pub(crate) struct ShortCapitalString(String);
19
20impl ShortCapitalString {
21    /// Maximum allowed string length.
22    pub const MAX_LENGTH: usize = 12;
23
24    /// Constructs a value from up to 12 uppercase ASCII Latin letters (`A`–`Z`).
25    ///
26    /// # Errors
27    /// Returns an error if:
28    /// - The number of characters is less than 1 or greater than 12.
29    /// - The string contains a character that is not uppercase ASCII.
30    pub fn from_ascii_uppercase(
31        string: impl Into<String>,
32    ) -> Result<Self, ShortCapitalStringError> {
33        let string = string.into();
34        let char_count = string.chars().count();
35        if char_count == 0 || char_count > Self::MAX_LENGTH {
36            return Err(ShortCapitalStringError::InvalidLength(char_count));
37        }
38        for character in string.chars() {
39            if !character.is_ascii_uppercase() {
40                return Err(ShortCapitalStringError::InvalidCharacter);
41            }
42        }
43        Ok(Self(string))
44    }
45
46    /// Constructs a value from up to 12 characters from `A`–`Z` and `_`.
47    ///
48    /// # Errors
49    /// Returns an error if:
50    /// - The number of characters is less than 1 or greater than 12.
51    /// - The string contains a character outside `A`–`Z` and `_`.
52    pub fn from_ascii_uppercase_and_underscore(
53        string: impl Into<String>,
54    ) -> Result<Self, ShortCapitalStringError> {
55        let string = string.into();
56        let char_count = string.chars().count();
57        if char_count == 0 || char_count > Self::MAX_LENGTH {
58            return Err(ShortCapitalStringError::InvalidLength(char_count));
59        }
60        for character in string.chars() {
61            if !character.is_ascii_uppercase() && character != '_' {
62                return Err(ShortCapitalStringError::InvalidCharacter);
63            }
64        }
65        Ok(Self(string))
66    }
67
68    /// Returns the [`Felt`] encoding of this string.
69    ///
70    /// The alphabet used in the encoding process is provided by the `alphabet` argument.
71    ///
72    /// **Contract:** `alphabet` must contain **ASCII characters only**. Then each character
73    /// occupies one UTF-8 byte, so the radix is [`str::len`] and matches the number of Unicode
74    /// scalars.
75    ///
76    /// The encoding is performed by multiplying the intermediate encoded value by the length of
77    /// the used alphabet and adding the relative index of each character. At the end of the
78    /// encoding process, the character length of the initial string is added to the encoded value.
79    ///
80    /// # Errors
81    /// Returns an error if:
82    /// - The string contains a character that is not part of the provided alphabet.
83    pub fn as_element(&self, alphabet: &str) -> Result<Felt, ShortCapitalStringError> {
84        debug_assert!(
85            alphabet.is_ascii(),
86            "ShortCapitalString::as_element: alphabet must be ASCII-only"
87        );
88        let alphabet_len = alphabet.len() as u64;
89        let mut encoded_value: u64 = 0;
90
91        for character in self.0.chars() {
92            let digit = alphabet
93                .chars()
94                .position(|c| c == character)
95                .map(|pos| pos as u64)
96                .ok_or(ShortCapitalStringError::InvalidCharacter)?;
97
98            encoded_value = encoded_value * alphabet_len + digit;
99        }
100
101        // Append the original length so decoding is unambiguous.
102        let char_len = self.0.chars().count() as u64;
103        encoded_value = encoded_value * alphabet_len + char_len;
104        Ok(Felt::new_unchecked(encoded_value))
105    }
106
107    /// Decodes an encoded [`Felt`] value into a [`ShortCapitalString`].
108    ///
109    /// `encoded_string` is the field element that carries the short-string encoding (as produced by
110    /// [`as_element`](Self::as_element)).
111    ///
112    /// The alphabet used in the decoding process is provided by the `alphabet` argument. The same
113    /// **ASCII-only** contract as [`as_element`](Self::as_element) applies; radix is [`str::len`].
114    ///
115    /// The decoding is performed by reading the encoded length from the least-significant digit,
116    /// then repeatedly taking modulus by alphabet length to recover each character index.
117    ///
118    /// # Errors
119    /// Returns an error if:
120    /// - The encoded value is outside of the provided `min_encoded_value..=max_encoded_value`.
121    /// - The decoded length is not between 1 and 12.
122    /// - Decoding leaves non-zero trailing data.
123    pub fn try_from_encoded_felt(
124        encoded_string: Felt,
125        alphabet: &str,
126        min_encoded_value: u64,
127        max_encoded_value: u64,
128    ) -> Result<Self, ShortCapitalStringError> {
129        let encoded_value = encoded_string.as_canonical_u64();
130        if encoded_value < min_encoded_value {
131            return Err(ShortCapitalStringError::ValueTooSmall(encoded_value));
132        }
133        if encoded_value > max_encoded_value {
134            return Err(ShortCapitalStringError::ValueTooLarge(encoded_value));
135        }
136
137        debug_assert!(
138            alphabet.is_ascii(),
139            "ShortCapitalString::try_from_encoded_felt: alphabet must be ASCII-only"
140        );
141        let alphabet_len = alphabet.len() as u64;
142        let mut remaining_value = encoded_value;
143        let string_len = (remaining_value % alphabet_len) as usize;
144        if string_len == 0 || string_len > Self::MAX_LENGTH {
145            return Err(ShortCapitalStringError::InvalidLength(string_len));
146        }
147        remaining_value /= alphabet_len;
148
149        let mut decoded = String::with_capacity(string_len);
150        for _ in 0..string_len {
151            let digit = (remaining_value % alphabet_len) as usize;
152            let character =
153                alphabet.chars().nth(digit).ok_or(ShortCapitalStringError::InvalidCharacter)?;
154            decoded.insert(0, character);
155            remaining_value /= alphabet_len;
156        }
157
158        if remaining_value != 0 {
159            return Err(ShortCapitalStringError::DataNotFullyDecoded);
160        }
161
162        Ok(Self(decoded))
163    }
164}
165
166impl fmt::Display for ShortCapitalString {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        f.write_str(&self.0)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use alloc::string::{String, ToString};
175
176    use assert_matches::assert_matches;
177
178    use super::{Felt, ShortCapitalString};
179    use crate::errors::ShortCapitalStringError;
180
181    #[test]
182    fn short_capital_string_encode_decode_roundtrip() {
183        let short_string = ShortCapitalString::from_ascii_uppercase("MIDEN").unwrap();
184        let encoded = short_string.as_element("ABCDEFGHIJKLMNOPQRSTUVWXYZ").unwrap();
185        let decoded = ShortCapitalString::try_from_encoded_felt(
186            encoded,
187            "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
188            1,
189            2481152873203736562,
190        )
191        .unwrap();
192        assert_eq!(decoded.to_string(), "MIDEN");
193
194        let name = String::from("MIDEN");
195        let from_name = ShortCapitalString::from_ascii_uppercase(name).unwrap();
196        assert_eq!(from_name.to_string(), "MIDEN");
197    }
198
199    #[test]
200    fn short_capital_string_rejects_invalid_values() {
201        assert_matches!(
202            ShortCapitalString::from_ascii_uppercase("").unwrap_err(),
203            ShortCapitalStringError::InvalidLength(0)
204        );
205        assert_matches!(
206            ShortCapitalString::from_ascii_uppercase("ABCDEFGHIJKLM").unwrap_err(),
207            ShortCapitalStringError::InvalidLength(13)
208        );
209        assert_matches!(
210            ShortCapitalString::from_ascii_uppercase("A_B").unwrap_err(),
211            ShortCapitalStringError::InvalidCharacter
212        );
213
214        assert_matches!(
215            ShortCapitalString::from_ascii_uppercase_and_underscore("MINTER-ADMIN").unwrap_err(),
216            ShortCapitalStringError::InvalidCharacter
217        );
218
219        let err = ShortCapitalString::try_from_encoded_felt(
220            Felt::ZERO,
221            "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
222            1,
223            2481152873203736562,
224        )
225        .unwrap_err();
226        assert_matches!(err, ShortCapitalStringError::ValueTooSmall(0));
227    }
228}