Skip to content

Commit 58b6eb7

Browse files
Remove regex requirement (#876)
* regex dep over safe-regex * new error variant * refactor regex use * coin parser wo regex * rm regex dep * fix incorrect conditions * add Coin::new * rm blackslash from valid charset * rewrite coin tests using rstest * fix broken test for denom with backslash * add name to testcases and add new tests * expect test panic message * addd tests with plus sign * update regex comment * simplify impl logic * implicit from_str * slash delim test * add changelog entry * version bump for ref link Co-authored-by: Farhad Shabani <[email protected]> Signed-off-by: Rano | Ranadeep <[email protected]> * refactor denom parsing * refactor tests without Coin::new * rm Coin::new * nit --------- Signed-off-by: Rano | Ranadeep <[email protected]> Co-authored-by: Farhad Shabani <[email protected]>
1 parent aed00a0 commit 58b6eb7

File tree

3 files changed

+95
-62
lines changed

3 files changed

+95
-62
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Remove `safe-regex` dependency
2+
([\#875](https://github.com/cosmos/ibc-rs/issues/875))

crates/ibc/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ serde_json = { version = "1", default-features = false, optional = true }
6161
tracing = { version = "0.1.36", default-features = false }
6262
prost = { version = "0.11", default-features = false }
6363
bytes = { version = "1.2.1", default-features = false }
64-
safe-regex = { version = "0.2.5", default-features = false }
6564
subtle-encoding = { version = "0.5", default-features = false }
6665
sha2 = { version = "0.10.6", default-features = false }
6766
displaydoc = { version = "0.2", default-features = false }

crates/ibc/src/applications/transfer/coin.rs

Lines changed: 93 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
//! Defines coin types; the objects that are being transferred.
22
33
use core::fmt::{Display, Error as FmtError, Formatter};
4-
use core::str::{from_utf8, FromStr};
4+
use core::str::FromStr;
55

66
use ibc_proto::cosmos::base::v1beta1::Coin as ProtoCoin;
7-
use safe_regex::regex;
87

98
use super::amount::Amount;
109
use super::denom::{BaseDenom, PrefixedDenom};
@@ -19,6 +18,9 @@ pub type BaseCoin = Coin<BaseDenom>;
1918

2019
pub type RawCoin = Coin<String>;
2120

21+
/// Allowed characters in string representation of a denomination.
22+
const VALID_DENOM_CHARACTERS: &str = "/:._-";
23+
2224
/// Coin defines a token with a denomination and an amount.
2325
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2426
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
@@ -58,25 +60,31 @@ where
5860
// Denominations can be 3 ~ 128 characters long and support letters, followed by either
5961
// a letter, a number or a separator ('/', ':', '.', '_' or '-').
6062
// Loosely copy the regex from here:
61-
// https://github.com/cosmos/cosmos-sdk/blob/v0.45.5/types/coin.go#L760-L762
62-
let matcher = regex!(br"([0-9]+)([a-zA-Z0-9/:\\._\x2d]+)");
63-
64-
let (m1, m2) = matcher.match_slices(coin_str.as_bytes()).ok_or_else(|| {
65-
TokenTransferError::InvalidCoin {
63+
// https://github.com/cosmos/cosmos-sdk/blob/v0.47.5/types/coin.go#L838-L840
64+
//
65+
// equivalent regex code in rust:
66+
// let re = Regex::new(r"^(?<amount>[0-9]+)(?<denom>[a-zA-Z0-9/:._-]+)$")?;
67+
// let cap = re.captures("123stake")?;
68+
// let (amount, denom) = (cap.name("amount")?.as_str(), cap.name("denom")?.as_str());
69+
70+
let (amount, denom) = coin_str
71+
.chars()
72+
.position(|x| !x.is_numeric())
73+
.map(|index| coin_str.split_at(index))
74+
.filter(|(amount, _)| !amount.is_empty())
75+
.filter(|(_, denom)| {
76+
denom
77+
.chars()
78+
.all(|x| x.is_alphanumeric() || VALID_DENOM_CHARACTERS.contains(x))
79+
})
80+
.ok_or_else(|| TokenTransferError::InvalidCoin {
6681
coin: coin_str.to_string(),
67-
}
68-
})?;
82+
})?;
6983

70-
let amount = from_utf8(m1)
71-
.map_err(TokenTransferError::Utf8Decode)?
72-
.parse()?;
73-
74-
let denom = from_utf8(m2)
75-
.map_err(TokenTransferError::Utf8Decode)?
76-
.parse()
77-
.map_err(Into::into)?;
78-
79-
Ok(Coin { amount, denom })
84+
Ok(Coin {
85+
amount: amount.parse()?,
86+
denom: denom.parse().map_err(Into::into)?,
87+
})
8088
}
8189
}
8290

@@ -119,53 +127,77 @@ impl<D: Display> Display for Coin<D> {
119127

120128
#[cfg(test)]
121129
mod tests {
122-
use super::*;
130+
use primitive_types::U256;
131+
use rstest::rstest;
123132

124-
#[test]
125-
fn test_parse_raw_coin() -> Result<(), TokenTransferError> {
126-
{
127-
let coin = RawCoin::from_str("123stake")?;
128-
assert_eq!(coin.denom, "stake");
129-
assert_eq!(coin.amount, 123u64.into());
130-
}
131-
132-
{
133-
let coin = RawCoin::from_str("1a1")?;
134-
assert_eq!(coin.denom, "a1");
135-
assert_eq!(coin.amount, 1u64.into());
136-
}
137-
138-
{
139-
let coin = RawCoin::from_str("0x1/:.\\_-")?;
140-
assert_eq!(coin.denom, "x1/:.\\_-");
141-
assert_eq!(coin.amount, 0u64.into());
142-
}
133+
use super::*;
143134

144-
{
145-
// `!` is not allowed
146-
let res = RawCoin::from_str("0x!");
147-
assert!(res.is_err());
148-
}
135+
#[rstest]
136+
#[case::nat("123stake", 123, "stake")]
137+
#[case::zero("0stake", 0, "stake")]
138+
#[case::u256_max(
139+
"115792089237316195423570985008687907853269984665640564039457584007913129639935stake",
140+
U256::MAX,
141+
"stake"
142+
)]
143+
#[case::digit_in_denom("1a1", 1, "a1")]
144+
#[case::chars_in_denom("0x1/:._-", 0, "x1/:._-")]
145+
#[case::ibc_denom("1234ibc/a0B1C", 1234, "ibc/a0B1C")]
146+
fn test_parse_raw_coin(
147+
#[case] parsed: RawCoin,
148+
#[case] amount: impl Into<Amount>,
149+
#[case] denom: &str,
150+
) {
151+
assert_eq!(
152+
parsed,
153+
RawCoin {
154+
denom: denom.into(),
155+
amount: amount.into()
156+
}
157+
);
158+
}
149159

160+
#[rstest]
161+
#[case::pos("+123stake")]
162+
#[case::pos_zero("+0stake")]
163+
#[case::neg("-123stake")]
164+
#[case::neg_zero("-0stake")]
165+
#[case::u256_max_plus_1(
166+
"115792089237316195423570985008687907853269984665640564039457584007913129639936stake"
167+
)]
168+
#[case::invalid_char_in_denom("0x!")]
169+
#[case::blackslash_in_denom("0x1/:.\\_-")]
170+
#[should_panic]
171+
fn test_failed_parse_raw_coin(#[case] _raw: RawCoin) {}
172+
173+
#[rstest]
174+
#[case::nomal("123stake,1a1,999den0m", &[(123, "stake"), (1, "a1"), (999, "den0m")])]
175+
#[case::tricky("123stake,1a1-999den0m", &[(123, "stake"), (1, "a1-999den0m")])]
176+
#[case::colon_delimiter("123stake:1a1:999den0m", &[(123, "stake:1a1:999den0m")])]
177+
#[case::dash_delimiter("123stake-1a1-999den0m", &[(123, "stake-1a1-999den0m")])]
178+
#[case::slash_delimiter("123stake/1a1/999den0m", &[(123, "stake/1a1/999den0m")])]
179+
fn test_parse_raw_coin_list(
180+
#[case] coins_str: &str,
181+
#[case] coins: &[(u64, &str)],
182+
) -> Result<(), TokenTransferError> {
183+
assert_eq!(
184+
RawCoin::from_string_list(coins_str)?,
185+
coins
186+
.iter()
187+
.map(|&(amount, denom)| RawCoin {
188+
denom: denom.to_string(),
189+
amount: amount.into(),
190+
})
191+
.collect::<Vec<_>>()
192+
);
150193
Ok(())
151194
}
152195

153-
#[test]
154-
fn test_parse_raw_coin_list() -> Result<(), TokenTransferError> {
155-
{
156-
let coins = RawCoin::from_string_list("123stake,1a1,999den0m")?;
157-
assert_eq!(coins.len(), 3);
158-
159-
assert_eq!(coins[0].denom, "stake");
160-
assert_eq!(coins[0].amount, 123u64.into());
161-
162-
assert_eq!(coins[1].denom, "a1");
163-
assert_eq!(coins[1].amount, 1u64.into());
164-
165-
assert_eq!(coins[2].denom, "den0m");
166-
assert_eq!(coins[2].amount, 999u64.into());
167-
}
168-
169-
Ok(())
196+
#[rstest]
197+
#[case::semicolon_delimiter("123stake;1a1;999den0m")]
198+
#[case::mixed_delimiter("123stake,1a1;999den0m")]
199+
#[should_panic(expected = "parsing failure in test")]
200+
fn test_failed_parse_raw_coin_list(#[case] coins_str: &str) {
201+
RawCoin::from_string_list(coins_str).expect("parsing failure in test");
170202
}
171203
}

0 commit comments

Comments
 (0)