Skip to content

Commit ff3c89a

Browse files
authored
feat: add cairo short string logic (#139)
1 parent cd968fd commit ff3c89a

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed

crates/starknet-types-core/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ pub mod curve;
66
pub mod hash;
77

88
pub mod felt;
9+
10+
#[cfg(any(feature = "std", feature = "alloc"))]
11+
pub mod short_string;
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//! Cairo short string
2+
//!
3+
//! The cairo language make it possible to create `Felt` values at compile time from so-called "short string".
4+
//! See https://docs.starknet.io/archive/cairo-101/strings/ for more information and syntax.
5+
//!
6+
//! This modules allows to mirror this behaviour in Rust, by leveraging type safety.
7+
//! A `ShortString` is string that have been checked and is guaranted to be convertible into a valid `Felt`.
8+
//! It checks that the `String` only contains ascii characters and is no longer than 31 characters.
9+
//!
10+
//! The convesion to `Felt` is done by using the internal ascii short string as bytes and parse those as a big endian number.
11+
12+
#[cfg(not(feature = "std"))]
13+
use crate::felt::alloc::string::{String, ToString};
14+
use crate::felt::Felt;
15+
16+
/// A cairo short string
17+
///
18+
/// Allow for safe conversion of cairo short string `String` into `Felt`,
19+
/// as it is guaranted that the value it contains can be represented as a felt.
20+
#[repr(transparent)]
21+
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
22+
pub struct ShortString(String);
23+
24+
impl core::fmt::Display for ShortString {
25+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
26+
self.0.fmt(f)
27+
}
28+
}
29+
30+
impl From<ShortString> for Felt {
31+
fn from(ss: ShortString) -> Self {
32+
let bytes = ss.0.as_bytes();
33+
34+
let mut buffer = [0u8; 32];
35+
// `ShortString` initialization guarantee that the string is ascii and its len doesn't exceed 31.
36+
// Which mean that its bytes representation won't either exceed 31 bytes.
37+
// So, this won't panic.
38+
buffer[(32 - bytes.len())..].copy_from_slice(bytes);
39+
40+
// The conversion will never fail
41+
Felt::from_bytes_be(&buffer)
42+
}
43+
}
44+
45+
#[derive(Debug, Clone)]
46+
#[cfg_attr(test, derive(PartialEq, Eq))]
47+
pub enum TryShortStringFromStringError {
48+
TooLong,
49+
NonAscii,
50+
}
51+
52+
impl core::fmt::Display for TryShortStringFromStringError {
53+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
54+
match self {
55+
TryShortStringFromStringError::TooLong => "string to long",
56+
TryShortStringFromStringError::NonAscii => "string contains non ascii characters",
57+
}
58+
.fmt(f)
59+
}
60+
}
61+
62+
#[cfg(feature = "std")]
63+
impl std::error::Error for TryShortStringFromStringError {}
64+
65+
impl TryFrom<String> for ShortString {
66+
type Error = TryShortStringFromStringError;
67+
68+
fn try_from(value: String) -> Result<Self, Self::Error> {
69+
if value.len() > 31 {
70+
return Err(TryShortStringFromStringError::TooLong);
71+
}
72+
if !value.as_str().is_ascii() {
73+
return Err(TryShortStringFromStringError::NonAscii);
74+
}
75+
76+
Ok(ShortString(value))
77+
}
78+
}
79+
80+
impl Felt {
81+
/// Create a felt value from a cairo short string.
82+
///
83+
/// The string must contains only ascii characters
84+
/// and its length must not exceed 31.
85+
///
86+
/// The returned felt value be that of the input raw bytes.
87+
pub fn parse_cairo_short_string(string: &str) -> Result<Self, TryShortStringFromStringError> {
88+
let bytes = string.as_bytes();
89+
if !bytes.is_ascii() {
90+
return Err(TryShortStringFromStringError::NonAscii);
91+
}
92+
if bytes.len() > 31 {
93+
return Err(TryShortStringFromStringError::TooLong);
94+
}
95+
96+
let mut buffer = [0u8; 32];
97+
buffer[(32 - bytes.len())..].copy_from_slice(bytes);
98+
99+
// The conversion will never fail
100+
Ok(Felt::from_bytes_be(&buffer))
101+
}
102+
}
103+
104+
impl TryFrom<&str> for ShortString {
105+
type Error = TryShortStringFromStringError;
106+
107+
fn try_from(value: &str) -> Result<Self, Self::Error> {
108+
if value.len() > 31 {
109+
return Err(TryShortStringFromStringError::TooLong);
110+
}
111+
if !value.is_ascii() {
112+
return Err(TryShortStringFromStringError::NonAscii);
113+
}
114+
115+
Ok(ShortString(value.to_string()))
116+
}
117+
}
118+
119+
#[cfg(test)]
120+
mod tests {
121+
use super::*;
122+
123+
#[test]
124+
fn ok() {
125+
for (string, expected_felt) in [
126+
(String::default(), Felt::ZERO),
127+
(String::from("aa"), Felt::from_hex_unchecked("0x6161")),
128+
(
129+
String::from("approve"),
130+
Felt::from_hex_unchecked("0x617070726f7665"),
131+
),
132+
(
133+
String::from("SN_SEPOLIA"),
134+
Felt::from_raw([
135+
507980251676163170,
136+
18446744073709551615,
137+
18446744073708869172,
138+
1555806712078248243,
139+
]),
140+
),
141+
] {
142+
let felt = Felt::parse_cairo_short_string(&string).unwrap();
143+
let short_string = ShortString::try_from(string.clone()).unwrap();
144+
145+
assert_eq!(felt, expected_felt);
146+
assert_eq!(short_string.0, string);
147+
assert_eq!(Felt::from(short_string), expected_felt);
148+
}
149+
}
150+
151+
#[test]
152+
fn ko_too_long() {
153+
let ok_string = String::from("This is a 31 characters string.");
154+
assert!(Felt::parse_cairo_short_string(&ok_string).is_ok());
155+
assert!(ShortString::try_from(ok_string).is_ok());
156+
157+
let ko_string = String::from("This is a 32 characters string..");
158+
159+
assert_eq!(
160+
Felt::parse_cairo_short_string(&ko_string),
161+
Err(TryShortStringFromStringError::TooLong)
162+
);
163+
assert_eq!(
164+
ShortString::try_from(ko_string),
165+
Err(TryShortStringFromStringError::TooLong)
166+
);
167+
}
168+
169+
#[test]
170+
fn ko_non_ascii() {
171+
let string = String::from("What a nice emoji 💫");
172+
173+
assert_eq!(
174+
Felt::parse_cairo_short_string(&string),
175+
Err(TryShortStringFromStringError::NonAscii)
176+
);
177+
assert_eq!(
178+
ShortString::try_from(string),
179+
Err(TryShortStringFromStringError::NonAscii)
180+
);
181+
}
182+
}

0 commit comments

Comments
 (0)