diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index dea51432227f..69cea1943593 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -4798,6 +4798,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "indexOf", + "description": "Returns the index of the first occurrence of a `key` in an `input` string.\nReturns `NOT_FOUND` (i.e. `type(uint256).max`) if the `key` is not found.\nReturns 0 in case of an empty `key`.", + "declaration": "function indexOf(string memory input, string memory key) external pure returns (uint256);", + "visibility": "external", + "mutability": "pure", + "signature": "indexOf(string,string)", + "selector": "0x8a0807b7", + "selectorBytes": [ + 138, + 8, + 7, + 183 + ] + }, + "group": "string", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "isDir", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 8f0de363a7fa..cb4ec7304ae1 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -1678,6 +1678,11 @@ interface Vm { /// 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); + /// Returns the index of the first occurrence of a `key` in an `input` string. + /// Returns `NOT_FOUND` (i.e. `type(uint256).max`) if the `key` is not found. + /// Returns 0 in case of an empty `key`. + #[cheatcode(group = String)] + function indexOf(string memory input, string memory key) external pure returns (uint256); // ======== JSON Parsing and Manipulation ======== diff --git a/crates/cheatcodes/src/string.rs b/crates/cheatcodes/src/string.rs index b7992b9450fd..c98560a72f06 100644 --- a/crates/cheatcodes/src/string.rs +++ b/crates/cheatcodes/src/string.rs @@ -2,6 +2,7 @@ use crate::{Cheatcode, Cheatcodes, Result, Vm::*}; use alloy_dyn_abi::{DynSolType, DynSolValue}; +use alloy_primitives::U256; use alloy_sol_types::SolValue; // address @@ -135,6 +136,14 @@ impl Cheatcode for splitCall { } } +// indexOf +impl Cheatcode for indexOfCall { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + let Self { input, key } = self; + Ok(input.find(key).map(U256::from).unwrap_or(U256::MAX).abi_encode()) + } +} + pub(super) fn parse(s: &str, ty: &DynSolType) -> Result { parse_value(s, ty).map(|v| v.abi_encode()) } diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 2116acf2b8ff..aff05c278740 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -237,6 +237,7 @@ interface Vm { function getNonce(address account) external view returns (uint64 nonce); function getNonce(Wallet calldata wallet) external returns (uint64 nonce); function getRecordedLogs() external returns (Log[] memory logs); + function indexOf(string memory input, string memory key) external pure returns (uint256); function isDir(string calldata path) external returns (bool result); function isFile(string calldata path) external returns (bool result); function isPersistent(address account) external view returns (bool persistent); diff --git a/testdata/default/cheats/StringUtils.t.sol b/testdata/default/cheats/StringUtils.t.sol index 471d628be018..136164a413d7 100644 --- a/testdata/default/cheats/StringUtils.t.sol +++ b/testdata/default/cheats/StringUtils.t.sol @@ -39,4 +39,16 @@ contract StringManipulationTest is DSTest { assertEq("World", splitResult[1]); assertEq("Reth", splitResult[2]); } + + function testIndexOf() public { + string memory input = "Hello, World!"; + string memory key1 = "Hello,"; + string memory key2 = "World!"; + string memory key3 = ""; + string memory key4 = "foundry"; + assertEq(vm.indexOf(input, key1), 0); + assertEq(vm.indexOf(input, key2), 7); + assertEq(vm.indexOf(input, key3), 0); + assertEq(vm.indexOf(input, key4), type(uint256).max); + } }