Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

native string utils cheatcodes #6891

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions crates/cheatcodes/assets/cheatcodes.json

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

16 changes: 16 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,22 @@ interface Vm {
#[cheatcode(group = String)]
function parseBool(string calldata stringifiedValue) external pure returns (bool parsedValue);

/// Converts the given `string` value to Lowercase.
#[cheatcode(group = String)]
function toLowercase(string calldata input) external pure returns (string memory output);
/// Converts the given `string` value to Uppercase.
#[cheatcode(group = String)]
function toUppercase(string calldata input) external pure returns (string memory output);
/// Trims leading and trailing whitespace from the given `string` value.
#[cheatcode(group = String)]
function trim(string calldata input) external pure returns (string memory output);
/// Replaces occurrences of `from` in the given `string` with `to`.
#[cheatcode(group = String)]
function replace(string calldata input, string calldata from, string calldata to) external pure returns (string memory output);
/// Splits the given `string` into an array of strings divided by the `delimiter`.
#[cheatcode(group = String)]
function split(string calldata input, string calldata delimiter) external pure returns (string[] memory outputs);

// ======== JSON Parsing and Manipulation ========

// -------- Reading --------
Expand Down
86 changes: 65 additions & 21 deletions crates/cheatcodes/src/string.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Implementations of [`String`](crate::Group::String) cheatcodes.

use crate::{Cheatcode, Cheatcodes, Result, Vm::*};
use alloy_dyn_abi::{DynSolType, DynSolValue};
use crate::{ Cheatcode, Cheatcodes, Result, Vm::* };
use alloy_dyn_abi::{ DynSolType, DynSolValue };
use alloy_sol_types::SolValue;

// address
Expand Down Expand Up @@ -94,22 +94,63 @@ impl Cheatcode for parseBoolCall {
}
}

// toLowercase
impl Cheatcode for toLowercaseCall {
fn apply(&self, _state: &mut Cheatcodes) -> Result {
let Self { input } = self;
Ok(input.to_lowercase().abi_encode())
}
}

// toUppercase
impl Cheatcode for toUppercaseCall {
fn apply(&self, _state: &mut Cheatcodes) -> Result {
let Self { input } = self;
Ok(input.to_uppercase().abi_encode())
}
}

// trim
impl Cheatcode for trimCall {
fn apply(&self, _state: &mut Cheatcodes) -> Result {
let Self { input } = self;
Ok(input.trim().abi_encode())
}
}

// Replace
impl Cheatcode for replaceCall {
fn apply(&self, _state: &mut Cheatcodes) -> Result {
let Self { input, from, to } = self;
Ok(input.replace(from, to).abi_encode())
}
}

// Split
impl Cheatcode for splitCall {
fn apply(&self, _state: &mut Cheatcodes) -> Result {
let Self { input, delimiter } = self;
let parts: Vec<&str> = input.split(delimiter).collect();
Ok(parts.abi_encode())
}
}

pub(super) fn parse(s: &str, ty: &DynSolType) -> Result {
parse_value(s, ty).map(|v| v.abi_encode())
}

pub(super) fn parse_array<I, S>(values: I, ty: &DynSolType) -> Result
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
where I: IntoIterator<Item = S>, S: AsRef<str>
{
let mut values = values.into_iter();
match values.next() {
Some(first) if !first.as_ref().is_empty() => std::iter::once(first)
.chain(values)
.map(|s| parse_value(s.as_ref(), ty))
.collect::<Result<Vec<_>, _>>()
.map(|vec| DynSolValue::Array(vec).abi_encode()),
Some(first) if !first.as_ref().is_empty() =>
std::iter
::once(first)
.chain(values)
.map(|s| parse_value(s.as_ref(), ty))
.collect::<Result<Vec<_>, _>>()
.map(|vec| DynSolValue::Array(vec).abi_encode()),
// return the empty encoded Bytes when values is empty or the first element is empty
_ => Ok("".abi_encode()),
}
Expand All @@ -119,11 +160,12 @@ where
fn parse_value(s: &str, ty: &DynSolType) -> Result<DynSolValue> {
match ty.coerce_str(s) {
Ok(value) => Ok(value),
Err(e) => match parse_value_fallback(s, ty) {
Some(Ok(value)) => Ok(value),
Some(Err(e2)) => Err(fmt_err!("failed parsing {s:?} as type `{ty}`: {e2}")),
None => Err(fmt_err!("failed parsing {s:?} as type `{ty}`: {e}")),
},
Err(e) =>
match parse_value_fallback(s, ty) {
Some(Ok(value)) => Ok(value),
Some(Err(e2)) => Err(fmt_err!("failed parsing {s:?} as type `{ty}`: {e2}")),
None => Err(fmt_err!("failed parsing {s:?} as type `{ty}`: {e}")),
}
}
}

Expand All @@ -136,16 +178,18 @@ fn parse_value_fallback(s: &str, ty: &DynSolType) -> Option<Result<DynSolValue,
"0" => false,
s if s.eq_ignore_ascii_case("true") => true,
s if s.eq_ignore_ascii_case("false") => false,
_ => return None,
_ => {
return None;
}
};
return Some(Ok(DynSolValue::Bool(b)));
}
DynSolType::Int(_) |
DynSolType::Uint(_) |
DynSolType::FixedBytes(_) |
DynSolType::Bytes => {
| DynSolType::Int(_)
| DynSolType::Uint(_)
| DynSolType::FixedBytes(_)
| DynSolType::Bytes => {
if !s.starts_with("0x") && s.chars().all(|c| c.is_ascii_hexdigit()) {
return Some(Err("missing hex prefix (\"0x\") for hex string"))
return Some(Err("missing hex prefix (\"0x\") for hex string"));
}
}
_ => {}
Expand Down
42 changes: 42 additions & 0 deletions testdata/cheats/StringUtils.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.18;

import "ds-test/test.sol";
import "./Vm.sol";

contract StringManipulationTest is DSTest {
Vm constant vm = Vm(HEVM_ADDRESS);

function testToLowercase() public {
string memory original = "Hello World";
string memory lowercased = vm.toLowercase(original);
assertEq("hello world", lowercased);
}

function testToUppercase() public {
string memory original = "Hello World";
string memory uppercased = vm.toUppercase(original);
assertEq("HELLO WORLD", uppercased);
}

function testTrim() public {
string memory original = " Hello World ";
string memory trimmed = vm.trim(original);
assertEq("Hello World", trimmed);
}

function testReplace() public {
string memory original = "Hello World";
string memory replaced = vm.replace(original, "World", "Reth");
assertEq("Hello Reth", replaced);
}

function testSplit() public {
string memory original = "Hello,World,Reth";
string[] memory splitResult = vm.split(original, ",");
assertEq(3, splitResult.length);
assertEq("Hello", splitResult[0]);
assertEq("World", splitResult[1]);
assertEq("Reth", splitResult[2]);
}
}
5 changes: 5 additions & 0 deletions testdata/cheats/Vm.sol

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

Loading