Skip to main content

eosio_numstr/
name.rs

1use core::{char, convert::TryFrom, fmt};
2
3/// All possible characters that can be used in EOSIO names.
4pub const NAME_CHARS: [u8; 32] = *b".12345abcdefghijklmnopqrstuvwxyz";
5
6/// The maximum character length of an EOSIO name.
7pub const NAME_MAX_LEN: usize = 13;
8
9/// An error which can be returned when parsing an EOSIO name.
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub enum ParseNameError {
12    /// The name contains a disallowed character.
13    BadChar(u8),
14    /// The name is over the maximum allowed length.
15    TooLong,
16}
17
18impl fmt::Display for ParseNameError {
19    #[inline]
20    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
21        match *self {
22            Self::BadChar(c) => {
23                write!(f, "name contains invalid character '{}'", char::from(c))
24            }
25            Self::TooLong => {
26                write!(f, "name is too long, must be 13 chars or less")
27            }
28        }
29    }
30}
31
32/// Attempts to create an EOSIO name from an `Iterator`.
33///
34/// # Errors
35///
36/// Will return `Err` if the name contains invalid characters or is too long.
37///
38/// # Examples
39///
40/// ```
41/// use eosio_numstr::{name_from_bytes, ParseNameError};
42/// assert_eq!(name_from_bytes("".bytes()), Ok(0));
43/// assert_eq!(name_from_bytes("a".bytes()), Ok(3458764513820540928));
44/// assert_eq!(name_from_bytes("123456789012".bytes()), Err(ParseNameError::BadChar(b'6')));
45/// assert_eq!(name_from_bytes("123451234512".bytes()), Ok(614251535012020768));
46/// assert_eq!(name_from_bytes("123451234512j".bytes()), Ok(614251535012020783));
47/// assert_eq!(name_from_bytes("123451234512k".bytes()), Err(ParseNameError::BadChar(b'k')));
48/// assert_eq!(name_from_bytes("12345123451234".bytes()), Err(ParseNameError::TooLong));
49/// assert_eq!(name_from_bytes("eosio.token".bytes()), Ok(6138663591592764928));
50/// assert_eq!(name_from_bytes("eosio.token".bytes()), Ok(6138663591592764928));
51/// assert_eq!(name_from_bytes("eosio.bpay".bytes()), Ok(6138663581940940800));
52/// assert_eq!(name_from_bytes("A".bytes()), Err(ParseNameError::BadChar(b'A')));
53/// assert_eq!(name_from_bytes("TEST".bytes()), Err(ParseNameError::BadChar(b'T')));
54/// ```
55#[inline]
56pub fn name_from_bytes<I>(mut iter: I) -> Result<u64, ParseNameError>
57where
58    I: Iterator<Item = u8>,
59{
60    let mut value = 0_u64;
61    let mut len = 0_u64;
62
63    // Loop through up to 12 characters
64    while let Some(c) = iter.next() {
65        let v = char_to_value(c).ok_or_else(|| ParseNameError::BadChar(c))?;
66        value <<= 5;
67        value |= u64::from(v);
68        len += 1;
69
70        if len == 12 {
71            break;
72        }
73    }
74
75    if len == 0 {
76        return Ok(0);
77    }
78
79    value <<= 4 + 5 * (12 - len);
80
81    // Check if we have a 13th character
82    if let Some(c) = iter.next() {
83        let v = char_to_value(c).ok_or_else(|| ParseNameError::BadChar(c))?;
84
85        // The 13th character can only be 4 bits, it has to be between letters 'a' to 'j'
86        if v > 0x0F {
87            return Err(ParseNameError::BadChar(c));
88        }
89
90        value |= u64::from(v);
91
92        // Check if we have more than 13 characters
93        if iter.next().is_some() {
94            return Err(ParseNameError::TooLong);
95        }
96    }
97
98    Ok(value)
99}
100
101/// Converts a character to a symbol.
102fn char_to_value(c: u8) -> Option<u8> {
103    if c == b'.' {
104        Some(0)
105    } else if c >= b'1' && c <= b'5' {
106        Some(c - b'1' + 1)
107    } else if c >= b'a' && c <= b'z' {
108        Some(c - b'a' + 6)
109    } else {
110        None
111    }
112}
113
114/// Converts an EOSIO name into an array of UTF-8 characters.
115///
116/// # Examples
117///
118/// ```
119/// use eosio_numstr::name_to_bytes;
120/// assert_eq!(name_to_bytes(6138663591592764928), *b"eosio.token..");
121/// assert_eq!(name_to_bytes(6138663581940940800), *b"eosio.bpay...");
122/// assert_eq!(name_to_bytes(0), *b".............");
123/// assert_eq!(name_to_bytes(614251535012020768), *b"123451234512.");
124/// assert_eq!(name_to_bytes(614251535012020783), *b"123451234512j");
125/// ```
126#[inline]
127#[must_use]
128pub fn name_to_bytes(value: u64) -> [u8; NAME_MAX_LEN] {
129    let mut chars = [b'.'; NAME_MAX_LEN];
130    if value == 0 {
131        return chars;
132    }
133
134    let mask = 0xF800_0000_0000_0000;
135    let mut v = value;
136    for (i, c) in chars.iter_mut().enumerate() {
137        let index = (v & mask) >> (if i == 12 { 60 } else { 59 });
138        let index = usize::try_from(index).unwrap_or_default();
139        if let Some(v) = NAME_CHARS.get(index) {
140            *c = *v;
141        }
142        v <<= 5;
143    }
144    chars
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use core::str;
151    use proptest::prelude::*;
152
153    #[test]
154    fn from_bytes_to_bytes() {
155        proptest!(|(input in "[[1-5][a-z]\\.]{0,12}[[1-5][a-j]\\.]{0,1}")| {
156            let name = match name_from_bytes(input.bytes()) {
157                Ok(name) => name,
158                Err(error) => panic!("Failed with input '{}': {}", input, error),
159            };
160            let bytes = name_to_bytes(name);
161            let string = str::from_utf8(&bytes).expect("Failed to convert bytes into str");
162            prop_assert_eq!(
163                string,
164                format!("{:.<13}", input),
165                "Names don't match"
166            );
167        });
168    }
169
170    #[test]
171    fn from_bytes_too_long() {
172        proptest!(|(input in "[[1-5][a-z]\\.]{12}[[1-5][a-j]\\.]{2}")| {
173            let result = name_from_bytes(input.bytes());
174            prop_assert_eq!(
175                result,
176                Err(ParseNameError::TooLong),
177                "Should've gotten TooLong error"
178            );
179        });
180    }
181
182    #[test]
183    fn from_bytes_bad_char() {
184        proptest!(|(
185            input in "[[1-5][a-z]\\.]{11}",
186            bad_char in "[^[1-5][a-z]\\.]{1}"
187        )| {
188            let input = input + &bad_char;
189            let result = name_from_bytes(input.bytes());
190            let bad_char = bad_char.bytes().next().unwrap();
191            prop_assert_eq!(
192                result,
193                Err(ParseNameError::BadChar(bad_char)),
194                "Should've gotten BadChar error with char '{}'",
195                char::from(bad_char)
196            );
197        });
198    }
199
200    #[test]
201    fn from_bytes_bad_last_char() {
202        proptest!(|(
203            input in "[[1-5][a-z]\\.]{12}",
204            bad_char in "[^[1-5][a-j]\\.]{1}"
205        )| {
206            let input = input + &bad_char;
207            let result = name_from_bytes(input.bytes());
208            let bad_char = bad_char.bytes().next().unwrap();
209            prop_assert_eq!(
210                result,
211                Err(ParseNameError::BadChar(bad_char)),
212                "Should've gotten BadChar error with char '{}'",
213                char::from(bad_char)
214            );
215        });
216    }
217
218    #[test]
219    fn to_bytes_doesnt_crash() {
220        proptest!(|(input in 0_u64..)| {
221            let _ = name_to_bytes(input);
222        });
223    }
224}