soroban_env_common/
symbol.rs

1//! The [`Symbol`] type is designed for encoding short, unambiguous, single-word
2//! identifiers such as the names of contract functions or assets in the
3//! network. `Symbol`s only admit characters from the 63-character repertoire
4//! `[a-zA-Z0-9_]` -- latin-script alphabetic letters, digits, and underscores.
5//!
6//! There are three reasons for this type to be different from the general
7//! `String` type:
8//!
9//!   1. We provide a space-optimized "small" form ([`SymbolSmall`]) that uses
10//!      small 6-bit codes (since the character repertoire is only 63 characters
11//!      plus null) and bit-packs them into the body of a [`Val`] such that they
12//!      can be used to represent short identifiers (up to 9 characters long)
13//!      without allocating a host object at all, essentially as "machine
14//!      integers with a textual interpretation". This is an optimization, since
15//!      we expect contracts to use `Symbol`s heavily. When a `Symbol` is larger
16//!      than this, it overflows to a [`SymbolObject`] transparently, as with
17//!      the size-optiized small number types.
18//!
19//!   2. Unlike [`StringObject`](crate::StringObject)s, there is a reasonably
20//!      small maximum size for [`SymbolObject`]s, given by
21//!      [`SCSYMBOL_LIMIT`](crate::xdr::SCSYMBOL_LIMIT) (currently 32 bytes).
22//!      Having such a modest maximum size allows working with `Symbol`s
23//!      entirely in guest Wasm code without a heap allocator, using only
24//!      fixed-size buffers on the Wasm shadow stack. The [`SymbolStr`] type is
25//!      a convenience wrapper around such a buffer, that can be directly
26//!      created from a [`Symbol`], copying its bytes from the host if the
27//!      `Symbol` is a `SymbolObject` or unpacking its 6-bit codes if the
28//!      `Symbol` is a `SymbolSmall`. [`SymbolStr`] can also yield a standard
29//!      Rust `&str` allowing its use with many Rust core library functions.
30//!
31//!   3. We expect (though do not require) [`StringObject`](crate::StringObject)
32//!      to usually be interpreted as Unicode codepoints encoded in UTF-8.
33//!      Unicode characters unfortunately admit a wide variety of "confusables"
34//!      or "homoglyphs": characters that have different codes, but look the
35//!      same when rendered in many common fonts. In many contexts these
36//!      represent a significant security risk to end users -- for example by
37//!      confusing an asset named `USD` (encoded as the UTF-8 hex byte sequence
38//!      `[55 53 44]`) with a similar-looking but different asset named `ՍЅᎠ`
39//!      (encoded as `[d5 8d d0 85 e1 8e a0]`) -- and so we provide `Symbol` as
40//!      an alternative to `String` for use in contexts where users wish to
41//!      minimize such risks, by restricting the possible characters that can
42//!      occur.
43//!
44//! `SymbolSmall` values are packed into a 56 bits (the "body" part of a 64-bit
45//! `Val` word) with zero padding in the high-order bits rather than low-order
46//! bits. While this means that lexicographical ordering of `SymbolSmall` values
47//! does not coincide with simple integer ordering of `Val` bodies, it optimizes
48//! the space cost of `SymbolSmall` literals in Wasm bytecode, where all integer
49//! literals are encoded as variable-length little-endian values, using ULEB128.
50
51use crate::xdr::SCSYMBOL_LIMIT;
52use crate::{
53    declare_tag_based_small_and_object_wrappers, val::ValConvert, Compare, ConversionError, Env,
54    Tag, TryFromVal, Val,
55};
56use core::{cmp::Ordering, fmt::Debug, hash::Hash, str};
57
58declare_tag_based_small_and_object_wrappers!(Symbol, SymbolSmall, SymbolObject);
59
60/// Errors related to operations on the [SymbolObject] and [SymbolSmall] types.
61#[derive(Debug)]
62pub enum SymbolError {
63    /// Returned when attempting to form a [SymbolSmall] from a string with more
64    /// than 9 characters.
65    TooLong(usize),
66    /// Returned when attempting to form a [SymbolObject] or [SymbolSmall] from
67    /// a byte string with characters outside the range `[a-zA-Z0-9_]`.
68    BadChar(u8),
69    /// Malformed small symbol (upper two bits were set).
70    MalformedSmall,
71}
72
73impl core::fmt::Display for SymbolError {
74    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75        match self {
76            SymbolError::TooLong(len) => f.write_fmt(format_args!(
77                "symbol too long: length {len}, max {MAX_SMALL_CHARS}"
78            )),
79            SymbolError::BadChar(char) => f.write_fmt(format_args!(
80                "symbol bad char: encountered `{char}`, supported range [a-zA-Z0-9_]"
81            )),
82            SymbolError::MalformedSmall => f.write_str("malformed small symbol"),
83        }
84    }
85}
86
87impl From<SymbolError> for ConversionError {
88    fn from(_: SymbolError) -> Self {
89        ConversionError
90    }
91}
92
93extern crate static_assertions as sa;
94
95use super::val::BODY_BITS;
96
97// Small symbols admit 9 6-bit chars for 54 bits.
98
99pub(crate) const MAX_SMALL_CHARS: usize = 9;
100const CODE_BITS: usize = 6;
101const CODE_MASK: u64 = (1u64 << CODE_BITS) - 1;
102const SMALL_MASK: u64 = (1u64 << (MAX_SMALL_CHARS * CODE_BITS)) - 1;
103sa::const_assert!(CODE_MASK == 0x3f);
104sa::const_assert!(CODE_BITS * MAX_SMALL_CHARS + 2 == BODY_BITS);
105sa::const_assert!(SMALL_MASK == 0x003f_ffff_ffff_ffff);
106
107impl<E: Env> TryFromVal<E, &[u8]> for Symbol {
108    type Error = crate::Error;
109
110    fn try_from_val(env: &E, v: &&[u8]) -> Result<Self, Self::Error> {
111        // Optimization note: this should only ever call one conversion
112        // function based on the input slice length, currently slices
113        // with invalid characters get re-validated.
114        if let Ok(s) = SymbolSmall::try_from_bytes(v) {
115            Ok(s.into())
116        } else {
117            env.symbol_new_from_slice(v)
118                .map(Into::into)
119                .map_err(Into::into)
120        }
121    }
122}
123
124impl<E: Env> TryFromVal<E, &str> for Symbol {
125    type Error = crate::Error;
126
127    fn try_from_val(env: &E, v: &&str) -> Result<Self, Self::Error> {
128        Symbol::try_from_val(env, &v.as_bytes())
129    }
130}
131
132impl<E: Env> Compare<Symbol> for E {
133    type Error = E::Error;
134    fn compare(&self, a: &Symbol, b: &Symbol) -> Result<Ordering, Self::Error> {
135        let taga = a.0.get_tag();
136        let tagb = b.0.get_tag();
137        match taga.cmp(&tagb) {
138            Ordering::Equal => {
139                if taga == Tag::SymbolSmall {
140                    let ssa = unsafe { SymbolSmall::unchecked_from_val(a.0) };
141                    let ssb = unsafe { SymbolSmall::unchecked_from_val(b.0) };
142                    Ok(ssa.cmp(&ssb))
143                } else {
144                    let soa = unsafe { SymbolObject::unchecked_from_val(a.0) };
145                    let sob = unsafe { SymbolObject::unchecked_from_val(b.0) };
146                    self.compare(&soa, &sob)
147                }
148            }
149            other => Ok(other),
150        }
151    }
152}
153
154impl SymbolSmall {
155    #[doc(hidden)]
156    pub const fn try_from_body(body: u64) -> Result<Self, SymbolError> {
157        // check if bits 54 or 55 are set, if so return error.
158        // the other low 54 bits can have any value, they're all
159        // legal small symbols.
160        if body & SMALL_MASK != body {
161            Err(SymbolError::MalformedSmall)
162        } else {
163            Ok(unsafe { SymbolSmall::from_body(body) })
164        }
165    }
166}
167
168impl Symbol {
169    pub const fn try_from_small_bytes(b: &[u8]) -> Result<Self, SymbolError> {
170        match SymbolSmall::try_from_bytes(b) {
171            Ok(sym) => Ok(Symbol(sym.0)),
172            Err(e) => Err(e),
173        }
174    }
175
176    pub const fn try_from_small_str(s: &str) -> Result<Self, SymbolError> {
177        Self::try_from_small_bytes(s.as_bytes())
178    }
179
180    // This should not be generally available as it can easily panic.
181    #[cfg(feature = "testutils")]
182    pub const fn from_small_str(s: &str) -> Self {
183        Symbol(SymbolSmall::from_str(s).0)
184    }
185}
186
187impl Ord for SymbolSmall {
188    fn cmp(&self, other: &Self) -> Ordering {
189        Iterator::cmp(self.into_iter(), *other)
190    }
191}
192
193impl TryFrom<&[u8]> for SymbolSmall {
194    type Error = SymbolError;
195
196    fn try_from(b: &[u8]) -> Result<SymbolSmall, SymbolError> {
197        Self::try_from_bytes(b)
198    }
199}
200
201#[cfg(feature = "std")]
202use crate::xdr::StringM;
203#[cfg(feature = "std")]
204impl<const N: u32> TryFrom<StringM<N>> for SymbolSmall {
205    type Error = SymbolError;
206
207    fn try_from(v: StringM<N>) -> Result<Self, Self::Error> {
208        v.as_slice().try_into()
209    }
210}
211#[cfg(feature = "std")]
212impl<const N: u32> TryFrom<&StringM<N>> for SymbolSmall {
213    type Error = SymbolError;
214
215    fn try_from(v: &StringM<N>) -> Result<Self, Self::Error> {
216        v.as_slice().try_into()
217    }
218}
219
220impl SymbolSmall {
221    #[doc(hidden)]
222    pub const fn validate_byte(b: u8) -> Result<(), SymbolError> {
223        match Self::encode_byte(b) {
224            Ok(_) => Ok(()),
225            Err(e) => Err(e),
226        }
227    }
228
229    const fn encode_byte(b: u8) -> Result<u8, SymbolError> {
230        let v = match b {
231            b'_' => 1,
232            b'0'..=b'9' => 2 + (b - b'0'),
233            b'A'..=b'Z' => 12 + (b - b'A'),
234            b'a'..=b'z' => 38 + (b - b'a'),
235            _ => return Err(SymbolError::BadChar(b)),
236        };
237        Ok(v)
238    }
239
240    pub const fn try_from_bytes(bytes: &[u8]) -> Result<SymbolSmall, SymbolError> {
241        if bytes.len() > MAX_SMALL_CHARS {
242            return Err(SymbolError::TooLong(bytes.len()));
243        }
244        let mut n = 0;
245        let mut accum: u64 = 0;
246        while n < bytes.len() {
247            let v = match Self::encode_byte(bytes[n]) {
248                Ok(v) => v,
249                Err(e) => return Err(e),
250            };
251            accum <<= CODE_BITS;
252            accum |= v as u64;
253            n += 1;
254        }
255        Ok(unsafe { Self::from_body(accum) })
256    }
257
258    pub const fn try_from_str(s: &str) -> Result<SymbolSmall, SymbolError> {
259        Self::try_from_bytes(s.as_bytes())
260    }
261
262    #[doc(hidden)]
263    pub const unsafe fn get_body(&self) -> u64 {
264        self.0.get_body()
265    }
266
267    // This should not be generally available as it can easily panic.
268    #[cfg(feature = "testutils")]
269    pub const fn from_str(s: &str) -> SymbolSmall {
270        match Self::try_from_str(s) {
271            Ok(sym) => sym,
272            Err(SymbolError::TooLong(_)) => panic!("symbol too long"),
273            Err(SymbolError::BadChar(_)) => panic!("symbol bad char"),
274            Err(SymbolError::MalformedSmall) => panic!("malformed small symbol"),
275        }
276    }
277
278    pub fn to_str(&self) -> SymbolStr {
279        sa::const_assert!(SCSYMBOL_LIMIT as usize >= MAX_SMALL_CHARS);
280        let mut chars = [b'\x00'; SCSYMBOL_LIMIT as usize];
281        for (src, dst) in self.into_iter().zip(chars.iter_mut()) {
282            *dst = src as u8
283        }
284        SymbolStr(chars)
285    }
286}
287
288/// An expanded form of a [Symbol] that stores its characters as ASCII-range
289/// bytes in a [u8] array -- up to the maximum size of a large symbol object --
290/// rather than as packed 6-bit codes within a [u64]. Useful for interoperation
291/// with standard Rust string types.
292#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
293pub struct SymbolStr([u8; SCSYMBOL_LIMIT as usize]);
294
295impl SymbolStr {
296    pub fn is_empty(&self) -> bool {
297        self.0[0] == 0
298    }
299    pub fn len(&self) -> usize {
300        let s: &[u8] = &self.0;
301        for (i, x) in s.iter().enumerate() {
302            if *x == 0 {
303                return i;
304            }
305        }
306        s.len()
307    }
308}
309
310impl Debug for SymbolStr {
311    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
312        let s: &str = self.as_ref();
313        f.debug_tuple("SymbolStr").field(&s).finish()
314    }
315}
316
317impl AsRef<[u8]> for SymbolStr {
318    fn as_ref(&self) -> &[u8] {
319        let s: &[u8] = &self.0;
320        &s[..self.len()]
321    }
322}
323
324// This conversion relies on `SymbolStr` representing a well-formed `Symbol`,
325// which in turn relies on `EnvBase` implementation to only produce valid
326// `Symbol`s.
327impl AsRef<str> for SymbolStr {
328    fn as_ref(&self) -> &str {
329        let s: &[u8] = self.as_ref();
330        unsafe { str::from_utf8_unchecked(s) }
331    }
332}
333
334impl From<&SymbolSmall> for SymbolStr {
335    fn from(s: &SymbolSmall) -> Self {
336        s.to_str()
337    }
338}
339
340impl From<SymbolSmall> for SymbolStr {
341    fn from(s: SymbolSmall) -> Self {
342        (&s).into()
343    }
344}
345
346impl<E: Env> TryFromVal<E, Symbol> for SymbolStr {
347    type Error = crate::Error;
348
349    fn try_from_val(env: &E, v: &Symbol) -> Result<Self, Self::Error> {
350        if let Ok(ss) = SymbolSmall::try_from(*v) {
351            Ok(ss.into())
352        } else {
353            let obj: SymbolObject = unsafe { SymbolObject::unchecked_from_val(v.0) };
354            let mut arr = [0u8; SCSYMBOL_LIMIT as usize];
355            let len: u32 = env.symbol_len(obj).map_err(Into::into)?.into();
356            if let Some(slice) = arr.get_mut(..len as usize) {
357                env.symbol_copy_to_slice(obj, Val::U32_ZERO, slice)
358                    .map_err(Into::into)?;
359                Ok(SymbolStr(arr))
360            } else {
361                Err(crate::Error::from_type_and_code(
362                    crate::xdr::ScErrorType::Value,
363                    crate::xdr::ScErrorCode::InternalError,
364                ))
365            }
366        }
367    }
368}
369
370#[cfg(feature = "std")]
371impl From<SymbolSmall> for String {
372    fn from(s: SymbolSmall) -> Self {
373        s.to_string()
374    }
375}
376#[cfg(feature = "std")]
377impl From<SymbolStr> for String {
378    fn from(s: SymbolStr) -> Self {
379        s.to_string()
380    }
381}
382#[cfg(feature = "std")]
383impl ToString for SymbolSmall {
384    fn to_string(&self) -> String {
385        self.into_iter().collect()
386    }
387}
388#[cfg(feature = "std")]
389impl ToString for SymbolStr {
390    fn to_string(&self) -> String {
391        let s: &str = self.as_ref();
392        s.to_string()
393    }
394}
395
396impl IntoIterator for SymbolSmall {
397    type Item = char;
398    type IntoIter = SymbolSmallIter;
399    fn into_iter(self) -> Self::IntoIter {
400        SymbolSmallIter(self.as_val().get_body())
401    }
402}
403
404/// An iterator that decodes the individual bit-packed characters from a
405/// symbol and yields them as regular Rust [char] values.
406#[repr(transparent)]
407#[derive(Clone)]
408pub struct SymbolSmallIter(u64);
409
410impl Iterator for SymbolSmallIter {
411    type Item = char;
412
413    fn next(&mut self) -> Option<Self::Item> {
414        while self.0 != 0 {
415            let res = match ((self.0 >> ((MAX_SMALL_CHARS - 1) * CODE_BITS)) & CODE_MASK) as u8 {
416                1 => b'_',
417                n @ (2..=11) => b'0' + n - 2,
418                n @ (12..=37) => b'A' + n - 12,
419                n @ (38..=63) => b'a' + n - 38,
420                _ => b'\0',
421            };
422            self.0 <<= CODE_BITS;
423            if res != b'\0' {
424                return Some(res as char);
425            }
426        }
427        None
428    }
429}
430
431#[cfg(feature = "testutils")]
432impl FromIterator<char> for SymbolSmall {
433    fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
434        let mut accum: u64 = 0;
435        for (n, i) in iter.into_iter().enumerate() {
436            if n >= MAX_SMALL_CHARS {
437                panic!("too many chars for SymbolSmall");
438            }
439            accum <<= CODE_BITS;
440            let v = match i {
441                '_' => 1,
442                '0'..='9' => 2 + ((i as u64) - ('0' as u64)),
443                'A'..='Z' => 12 + ((i as u64) - ('A' as u64)),
444                'a'..='z' => 38 + ((i as u64) - ('a' as u64)),
445                _ => break,
446            };
447            accum |= v;
448        }
449        unsafe { Self::from_body(accum) }
450    }
451}
452
453#[cfg(feature = "std")]
454use crate::xdr::{ScSymbol, ScVal};
455
456#[cfg(feature = "std")]
457impl TryFrom<ScVal> for SymbolSmall {
458    type Error = crate::Error;
459    fn try_from(v: ScVal) -> Result<Self, Self::Error> {
460        (&v).try_into()
461    }
462}
463#[cfg(feature = "std")]
464impl TryFrom<&ScVal> for SymbolSmall {
465    type Error = crate::Error;
466    fn try_from(v: &ScVal) -> Result<Self, Self::Error> {
467        if let ScVal::Symbol(ScSymbol(vec)) = v {
468            vec.try_into().map_err(Into::into)
469        } else {
470            Err(ConversionError.into())
471        }
472    }
473}
474
475#[cfg(feature = "std")]
476impl<E: Env> TryFromVal<E, ScVal> for Symbol {
477    type Error = crate::Error;
478
479    fn try_from_val(env: &E, v: &ScVal) -> Result<Self, Self::Error> {
480        Symbol::try_from_val(env, &v)
481    }
482}
483
484#[cfg(feature = "std")]
485impl<E: Env> TryFromVal<E, &ScVal> for Symbol {
486    type Error = crate::Error;
487    fn try_from_val(env: &E, v: &&ScVal) -> Result<Self, Self::Error> {
488        if let ScVal::Symbol(sym) = v {
489            Symbol::try_from_val(env, &sym)
490        } else {
491            Err(ConversionError.into())
492        }
493    }
494}
495
496#[cfg(feature = "std")]
497impl<E: Env> TryFromVal<E, ScSymbol> for Symbol {
498    type Error = crate::Error;
499
500    fn try_from_val(env: &E, v: &ScSymbol) -> Result<Self, Self::Error> {
501        Symbol::try_from_val(env, &v)
502    }
503}
504
505#[cfg(feature = "std")]
506impl<E: Env> TryFromVal<E, &ScSymbol> for Symbol {
507    type Error = crate::Error;
508    fn try_from_val(env: &E, v: &&ScSymbol) -> Result<Self, Self::Error> {
509        Symbol::try_from_val(env, &v.0.as_slice())
510    }
511}
512
513#[cfg(feature = "std")]
514impl TryFrom<SymbolSmall> for ScVal {
515    type Error = crate::Error;
516    fn try_from(s: SymbolSmall) -> Result<Self, crate::Error> {
517        let res: Result<Vec<u8>, _> = s.into_iter().map(<u8 as TryFrom<char>>::try_from).collect();
518        let vec = res.map_err(|_| {
519            crate::Error::from_type_and_code(
520                crate::xdr::ScErrorType::Value,
521                crate::xdr::ScErrorCode::InvalidInput,
522            )
523        })?;
524        Ok(ScVal::Symbol(vec.try_into()?))
525    }
526}
527
528#[cfg(feature = "std")]
529impl TryFrom<&SymbolSmall> for ScVal {
530    type Error = crate::Error;
531    fn try_from(s: &SymbolSmall) -> Result<Self, crate::Error> {
532        (*s).try_into()
533    }
534}
535
536#[cfg(feature = "std")]
537impl<E: Env> TryFromVal<E, Symbol> for ScVal {
538    type Error = crate::Error;
539    fn try_from_val(e: &E, s: &Symbol) -> Result<Self, crate::Error> {
540        Ok(ScVal::Symbol(ScSymbol::try_from_val(e, s)?))
541    }
542}
543
544#[cfg(feature = "std")]
545impl<E: Env> TryFromVal<E, Symbol> for ScSymbol {
546    type Error = crate::Error;
547    fn try_from_val(e: &E, s: &Symbol) -> Result<Self, crate::Error> {
548        let sstr = SymbolStr::try_from_val(e, s)?;
549        Ok(ScSymbol(sstr.0.as_slice()[0..sstr.len()].try_into()?))
550    }
551}
552
553#[cfg(test)]
554mod test_without_string {
555    use super::{SymbolSmall, SymbolStr};
556
557    #[test]
558    fn test_roundtrip() {
559        let input = "stellar";
560        let sym = SymbolSmall::try_from_str(input).unwrap();
561        let sym_str = SymbolStr::from(sym);
562        let s: &str = sym_str.as_ref();
563        assert_eq!(s, input);
564    }
565
566    #[test]
567    fn test_roundtrip_zero() {
568        let input = "";
569        let sym = SymbolSmall::try_from_str(input).unwrap();
570        let sym_str = SymbolStr::from(sym);
571        let s: &str = sym_str.as_ref();
572        assert_eq!(s, input);
573    }
574
575    #[test]
576    fn test_roundtrip_nine() {
577        let input = "123456789";
578        let sym = SymbolSmall::try_from_str(input).unwrap();
579        let sym_str = SymbolStr::from(sym);
580        let s: &str = sym_str.as_ref();
581        assert_eq!(s, input);
582    }
583
584    #[test]
585    fn test_enc() {
586        // Some exact test vectors to ensure the encoding is what we expect.
587        let vectors: &[(&str, u64)] = &[
588            ("a",           0b__000_000__000_000__000_000__000_000__000_000__000_000__000_000__000_000__100_110_u64),
589            ("ab",          0b__000_000__000_000__000_000__000_000__000_000__000_000__000_000__100_110__100_111_u64),
590            ("abc",         0b__000_000__000_000__000_000__000_000__000_000__000_000__100_110__100_111__101_000_u64),
591            ("ABC",         0b__000_000__000_000__000_000__000_000__000_000__000_000__001_100__001_101__001_110_u64),
592            ("____5678",    0b__000_000__000_001__000_001__000_001__000_001__000_111__001_000__001_001__001_010_u64),
593            ("____56789",   0b__000_001__000_001__000_001__000_001__000_111__001_000__001_001__001_010__001_011_u64),
594        ];
595        for (s, body) in vectors.iter() {
596            let sym = SymbolSmall::try_from_str(s).unwrap();
597            assert_eq!(unsafe { sym.get_body() }, *body);
598        }
599    }
600
601    #[test]
602    fn test_ord() {
603        let vals = ["Hello", "hello", "hellos", "", "_________", "________"];
604        for a in vals.iter() {
605            let a_sym = SymbolSmall::try_from_str(a).unwrap();
606            for b in vals.iter() {
607                let b_sym = SymbolSmall::try_from_str(b).unwrap();
608                assert_eq!(a.cmp(b), a_sym.cmp(&b_sym));
609            }
610        }
611    }
612}
613
614#[cfg(all(test, feature = "std"))]
615mod test_with_string {
616    use super::SymbolSmall;
617    use std::string::{String, ToString};
618
619    #[test]
620    fn test_roundtrip() {
621        let input = "stellar";
622        let sym = SymbolSmall::try_from_str(input).unwrap();
623        let s: String = sym.to_string();
624        assert_eq!(input, &s);
625    }
626
627    #[test]
628    fn test_roundtrip_zero() {
629        let input = "";
630        let sym = SymbolSmall::try_from_str(input).unwrap();
631        let s: String = sym.to_string();
632        assert_eq!(input, &s);
633    }
634
635    #[test]
636    fn test_roundtrip_nine() {
637        let input = "123456789";
638        let sym = SymbolSmall::try_from_str(input).unwrap();
639        let s: String = sym.to_string();
640        assert_eq!(input, &s);
641    }
642}