Skip to content

Commit

Permalink
sozo: support arrays in calldata arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
remybar committed Jan 16, 2025
1 parent ec00eca commit 808d6ad
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 18 deletions.
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion crates/dojo/world/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ num-bigint.workspace = true

[dev-dependencies]
tokio.workspace = true
futures.workspace = true

[features]
ipfs = [ "dep:ipfs-api-backend-hyper" ]
200 changes: 185 additions & 15 deletions crates/dojo/world/src/config/calldata_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,42 @@ pub enum CalldataDecoderError {

pub type DecoderResult<T, E = CalldataDecoderError> = Result<T, E>;

const ITEM_DELIMITER: char = ',';
const ITEM_DELIMITER: char = ' ';
const ARRAY_ITEM_DELIMITER: char = ',';
const SHORT_STRING_DELIMITER: char = '\'';
const STRING_DELIMITER: char = '\"';
const ITEM_PREFIX_DELIMITER: char = ':';

/// Remove the first and the last characters of a string.
fn remove_first_and_last_characters(value: &str) -> &str {
let mut chars = value.chars();
chars.next();
chars.next_back();
chars.as_str()
}

fn is_string_calldata(input: &str) -> bool {
input.starts_with(STRING_DELIMITER) && input.ends_with(STRING_DELIMITER)
}

fn is_short_string_calldata(input: &str) -> bool {
input.starts_with(SHORT_STRING_DELIMITER) && input.ends_with(SHORT_STRING_DELIMITER)
}

/// split a string using whitespaces except if they are inside quotes (single or double)
fn split_unquoted(input: &str, delimiter: char) -> Vec<&str> {
let mut quoted = false;

input
.split(|c| {
if [STRING_DELIMITER, SHORT_STRING_DELIMITER].contains(&c) {
quoted = !quoted;
}
c == delimiter && !quoted
})
.collect()
}

/// A trait for decoding calldata into a vector of Felts.
trait CalldataDecoder {
fn decode(&self, input: &str) -> DecoderResult<Vec<Felt>>;
Expand Down Expand Up @@ -105,25 +138,93 @@ impl CalldataDecoder for SignedIntegerCalldataDecoder {
}
}

/// Decodes a dynamic array into an array of [`Felt`].
/// Array items must fit on one felt.
struct DynamicArrayCalldataDecoder;
impl CalldataDecoder for DynamicArrayCalldataDecoder {
fn decode(&self, input: &str) -> DecoderResult<Vec<Felt>> {
let items = input.split(ARRAY_ITEM_DELIMITER).collect::<Vec<_>>();
let mut decoded_items: Vec<Felt> = vec![items.len().into()];

for item in items {
decoded_items.extend(DefaultCalldataDecoder.decode(item)?);
}

Ok(decoded_items)
}
}

/// Decodes a dynamic u256 array into an array of [`Felt`].
struct U256DynamicArrayCalldataDecoder;
impl CalldataDecoder for U256DynamicArrayCalldataDecoder {
fn decode(&self, input: &str) -> DecoderResult<Vec<Felt>> {
let items = input.split(ARRAY_ITEM_DELIMITER).collect::<Vec<_>>();
let mut decoded_items: Vec<Felt> = vec![items.len().into()];

for item in items {
decoded_items.extend(U256CalldataDecoder.decode(item)?);
}

Ok(decoded_items)
}
}

/// Decodes a fixed-size array into an array of [`Felt`].
/// Array items must fit on one felt.
struct FixedSizeArrayCalldataDecoder;
impl CalldataDecoder for FixedSizeArrayCalldataDecoder {
fn decode(&self, input: &str) -> DecoderResult<Vec<Felt>> {
let items = input.split(ARRAY_ITEM_DELIMITER).collect::<Vec<_>>();
let mut decoded_items: Vec<Felt> = vec![];

for item in items {
decoded_items.extend(DefaultCalldataDecoder.decode(item)?);
}

Ok(decoded_items)
}
}

/// Decodes a u256 fixed-size array into an array of [`Felt`].
struct U256FixedSizeArrayCalldataDecoder;
impl CalldataDecoder for U256FixedSizeArrayCalldataDecoder {
fn decode(&self, input: &str) -> DecoderResult<Vec<Felt>> {
let items = input.split(ARRAY_ITEM_DELIMITER).collect::<Vec<_>>();
let mut decoded_items: Vec<Felt> = vec![];

for item in items {
decoded_items.extend(U256CalldataDecoder.decode(item)?);
}

Ok(decoded_items)
}
}

/// Decodes a string into a [`Felt`], either from hexadecimal or decimal string.
struct DefaultCalldataDecoder;
impl CalldataDecoder for DefaultCalldataDecoder {
fn decode(&self, input: &str) -> DecoderResult<Vec<Felt>> {
let felt = if let Some(hex_str) = input.strip_prefix("0x") {
Felt::from_hex(hex_str)?
if is_string_calldata(input) {
StrCalldataDecoder.decode(remove_first_and_last_characters(input))
} else if is_short_string_calldata(input) {
ShortStrCalldataDecoder.decode(remove_first_and_last_characters(input))
} else {
Felt::from_dec_str(input)?
};
let felt = if let Some(hex_str) = input.strip_prefix("0x") {
Felt::from_hex(hex_str)?
} else {
Felt::from_dec_str(input)?
};

Ok(vec![felt])
Ok(vec![felt])
}
}
}

/// Decodes a string of calldata items into a vector of Felts.
///
/// # Arguments:
///
/// * `input` - The input string to decode, with each item separated by a comma. Inputs can have
/// * `input` - The input string to decode, with each item separated by a space. Inputs can have
/// prefixes to indicate the type of the item.
///
/// # Returns
Expand All @@ -132,11 +233,11 @@ impl CalldataDecoder for DefaultCalldataDecoder {
/// # Example
///
/// ```
/// let input = "u256:0x1,str:hello,64";
/// let input = "u256:0x1 \"hello world\" 64";
/// let result = decode_calldata(input).unwrap();
/// ```
pub fn decode_calldata(input: &str) -> DecoderResult<Vec<Felt>> {
let items = input.split(ITEM_DELIMITER);
let items = split_unquoted(input, ITEM_DELIMITER);
let mut calldata = vec![];

for item in items {
Expand All @@ -157,12 +258,16 @@ pub fn decode_calldata(input: &str) -> DecoderResult<Vec<Felt>> {
pub fn decode_single_calldata(item: &str) -> DecoderResult<Vec<Felt>> {
let item = item.trim();

println!("inner item: '{item}'");

let felts = if let Some((prefix, value)) = item.split_once(ITEM_PREFIX_DELIMITER) {
match prefix {
"u256" => U256CalldataDecoder.decode(value)?,
"str" => StrCalldataDecoder.decode(value)?,
"sstr" => ShortStrCalldataDecoder.decode(value)?,
"int" => SignedIntegerCalldataDecoder.decode(value)?,
"arr" => DynamicArrayCalldataDecoder.decode(value)?,
"u256arr" => U256DynamicArrayCalldataDecoder.decode(value)?,
"farr" => FixedSizeArrayCalldataDecoder.decode(value)?,
"u256farr" => U256FixedSizeArrayCalldataDecoder.decode(value)?,
_ => DefaultCalldataDecoder.decode(item)?,
}
} else {
Expand Down Expand Up @@ -197,7 +302,7 @@ mod tests {

#[test]
fn test_short_str_decoder() {
let input = "sstr:hello";
let input = "'hello'";
let expected = vec![cairo_short_string_to_felt("hello").unwrap()];

let result = decode_calldata(input).unwrap();
Expand All @@ -206,7 +311,7 @@ mod tests {

#[test]
fn test_str_decoder() {
let input = "str:hello";
let input = "\"hello\"";
let expected =
vec![0_u128.into(), cairo_short_string_to_felt("hello").unwrap(), 5_u128.into()];

Expand All @@ -216,7 +321,7 @@ mod tests {

#[test]
fn test_str_decoder_long() {
let input = "str:hello with spaces and a long string longer than 31 chars";
let input = "\"hello with spaces and a long string longer than 31 chars\"";

let expected = vec![
// Length of the data.
Expand Down Expand Up @@ -294,9 +399,66 @@ mod tests {
assert_eq!(result, expected);
}

#[test]
fn test_u8_dynamic_array() {
let input = "arr:1,2,3,1";

let expected = vec![
// Length of the array.
4.into(),
Felt::ONE,
Felt::TWO,
Felt::THREE,
Felt::ONE,
];

let result = decode_calldata(input).unwrap();
assert_eq!(result, expected);
}

#[test]
fn test_u8_fixed_size_array() {
let input = "farr:1,2,3,1";

let expected = vec![Felt::ONE, Felt::TWO, Felt::THREE, Felt::ONE];

let result = decode_calldata(input).unwrap();
assert_eq!(result, expected);
}

#[test]
fn test_u256_dynamic_array() {
let input = "u256arr:1,2,3";

let expected = vec![
// Length of the array.
3.into(),
Felt::ONE,
Felt::ZERO,
Felt::TWO,
Felt::ZERO,
Felt::THREE,
Felt::ZERO,
];

let result = decode_calldata(input).unwrap();
assert_eq!(result, expected);
}

#[test]
fn test_u256_fixed_size_array() {
let input = "u256farr:0x01,0x02,0x03";

let expected = vec![Felt::ONE, Felt::ZERO, Felt::TWO, Felt::ZERO, Felt::THREE, Felt::ZERO];

let result = decode_calldata(input).unwrap();
assert_eq!(result, expected);
}

#[test]
fn test_combined_decoders() {
let input = "u256:0x64,str:world,987654,0x123";
let input =
"u256:0x64 \"world\" 987654 0x123 'short string' \"very very very long string\"";
let expected = vec![
// U256 low.
100_u128.into(),
Expand All @@ -312,6 +474,14 @@ mod tests {
987654_u128.into(),
// Hex value.
291_u128.into(),
// Short string
cairo_short_string_to_felt("short string").unwrap(),
// Long string data len.
0_u128.into(),
// Long string pending word.
cairo_short_string_to_felt("very very very long string").unwrap(),
// Long string pending word len.
26_u128.into(),
];

let result = decode_calldata(input).unwrap();
Expand Down

0 comments on commit 808d6ad

Please sign in to comment.