feat(x/bank): Replace regex parsing of denom validation to generated parsing (#19511)

This commit is contained in:
Matt, Park
2024-03-07 09:34:49 +00:00
committed by GitHub
parent 4edf6b2a85
commit a1e3a85ae1
10 changed files with 301 additions and 32 deletions
+17 -23
View File
@@ -842,31 +842,16 @@ func (coins Coins) Sort() Coins {
return coins
}
//-----------------------------------------------------------------------------
// Parsing
var (
// Denominations can be 3 ~ 128 characters long and support letters, followed by either
// a letter, a number or a separator ('/', ':', '.', '_' or '-').
reDnmString = `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}`
reDecAmt = `[[:digit:]]+(?:\.[[:digit:]]+)?|\.[[:digit:]]+`
reSpc = `[[:space:]]*`
reDnm *regexp.Regexp
reDecCoin *regexp.Regexp
reDecAmt = `[[:digit:]]+(?:\.[[:digit:]]+)?|\.[[:digit:]]+`
reSpc = `[[:space:]]*`
coinDenomRegex func() string
reDnm *regexp.Regexp
reDecCoin *regexp.Regexp
)
func init() {
SetCoinDenomRegex(DefaultCoinDenomRegex)
}
// DefaultCoinDenomRegex returns the default regex string
func DefaultCoinDenomRegex() string {
return reDnmString
}
// coinDenomRegex returns the current regex string and can be overwritten for custom validation
var coinDenomRegex = DefaultCoinDenomRegex
// SetCoinDenomRegex allows for coin's custom validation by overriding the regular
// expression string used for denom validation.
func SetCoinDenomRegex(reFn func() string) {
@@ -878,9 +863,18 @@ func SetCoinDenomRegex(reFn func() string) {
// ValidateDenom is the default validation function for Coin.Denom.
func ValidateDenom(denom string) error {
if !reDnm.MatchString(denom) {
if reDnm == nil || reDecCoin == nil {
// Convert the string to a byte slice as required by the Ragel-generated function.
denomBytes := []byte(denom)
// Call the Ragel-generated function.
if !MatchDenom(denomBytes) {
return fmt.Errorf("invalid denom: %s", denom)
}
} else if !reDnm.MatchString(denom) { // If reDnm has been initialized, use it for matching.
return fmt.Errorf("invalid denom: %s", denom)
}
return nil
}
+173
View File
@@ -0,0 +1,173 @@
//line coin_regex.rl:1
// `coin_regex.go` is generated by regel using `ragel -Z coin_regex.rl`.
// do not directly edit `coin_regex.go`.
// source: types/coin_regex.rl
// nolint:gocritic,unused,ineffassign
// Regex parsing of denoms were as the following
// reDnmString = `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}`
// reDecAmt = `[[:digit:]]+(?:\.[[:digit:]]+)?|\.[[:digit:]]+`
// reSpc = `[[:space:]]*`
// reDnm = regexp.MustCompile(fmt.Sprintf(`^%s$`, coinDenomRegex()))
// reDecCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reDecAmt, reSpc, coinDenomRegex()))
package types
func MatchDenom(data []byte) bool {
var _scanner_actions []byte = []byte{
0, 1, 0,
}
var _scanner_key_offsets []byte = []byte{
0, 0, 4, 11,
}
var _scanner_trans_keys []byte = []byte{
65, 90, 97, 122, 95, 45, 58, 65,
90, 97, 122,
}
var _scanner_single_lengths []byte = []byte{
0, 0, 1, 0,
}
var _scanner_range_lengths []byte = []byte{
0, 2, 3, 0,
}
var _scanner_index_offsets []byte = []byte{
0, 0, 3, 8,
}
var _scanner_indicies []byte = []byte{
0, 0, 1, 2, 2, 2, 2, 1,
1,
}
var _scanner_trans_targs []byte = []byte{
2, 0, 3,
}
var _scanner_trans_actions []byte = []byte{
0, 0, 1,
}
const scanner_start int = 1
const scanner_first_final int = 3
const scanner_error int = 0
const scanner_en_main int = 1
if len(data) < 3 || len(data) > 128 {
return false
}
cs, p, pe, eof := 0, 0, len(data), len(data)
_ = eof
{
cs = scanner_start
}
{
var _klen int
var _trans int
var _acts int
var _nacts uint
var _keys int
if p == pe {
goto _test_eof
}
if cs == 0 {
goto _out
}
_resume:
_keys = int(_scanner_key_offsets[cs])
_trans = int(_scanner_index_offsets[cs])
_klen = int(_scanner_single_lengths[cs])
if _klen > 0 {
_lower := int(_keys)
var _mid int
_upper := int(_keys + _klen - 1)
for {
if _upper < _lower {
break
}
_mid = _lower + ((_upper - _lower) >> 1)
switch {
case data[p] < _scanner_trans_keys[_mid]:
_upper = _mid - 1
case data[p] > _scanner_trans_keys[_mid]:
_lower = _mid + 1
default:
_trans += int(_mid - int(_keys))
goto _match
}
}
_keys += _klen
_trans += _klen
}
_klen = int(_scanner_range_lengths[cs])
if _klen > 0 {
_lower := int(_keys)
var _mid int
_upper := int(_keys + (_klen << 1) - 2)
for {
if _upper < _lower {
break
}
_mid = _lower + (((_upper - _lower) >> 1) & ^1)
switch {
case data[p] < _scanner_trans_keys[_mid]:
_upper = _mid - 2
case data[p] > _scanner_trans_keys[_mid+1]:
_lower = _mid + 2
default:
_trans += int((_mid - int(_keys)) >> 1)
goto _match
}
}
_trans += _klen
}
_match:
_trans = int(_scanner_indicies[_trans])
cs = int(_scanner_trans_targs[_trans])
if _scanner_trans_actions[_trans] == 0 {
goto _again
}
_acts = int(_scanner_trans_actions[_trans])
_nacts = uint(_scanner_actions[_acts])
_acts++
for ; _nacts > 0; _nacts-- {
_acts++
switch _scanner_actions[_acts-1] {
case 0:
return true
}
}
_again:
if cs == 0 {
goto _out
}
p++
if p != pe {
goto _resume
}
_test_eof:
{
}
_out:
{
}
}
return false
}
+40
View File
@@ -0,0 +1,40 @@
// `coin_regex.go` is generated by regel using `ragel -Z coin_regex.rl`.
// do not directly edit `coin_regex.go`.
// source: types/coin_regex.rl
// nolint:gocritic,unused,ineffassign
// Regex parsing of denoms were as the following
// reDnmString = `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}`
// reDecAmt = `[[:digit:]]+(?:\.[[:digit:]]+)?|\.[[:digit:]]+`
// reSpc = `[[:space:]]*`
// reDnm = regexp.MustCompile(fmt.Sprintf(`^%s$`, coinDenomRegex()))
// reDecCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reDecAmt, reSpc, coinDenomRegex()))
package types
func MatchDenom(data []byte) bool {
%% machine scanner;
%% write data;
if len(data) < 3 || len(data) > 128 {
return false
}
cs, p, pe, eof := 0, 0, len(data), len(data)
_ = eof
%%{
# Define character classes
special = '/' | ':' | '.' | '_' | '-';
denom_pattern = [a-zA-Z] (alnum | special);
# Combined pattern for matching either a denomination or a decimal amount
main := denom_pattern @{ return true };
write init;
write exec;
}%%
return false
}
+3 -1
View File
@@ -108,6 +108,8 @@ func (s *coinTestSuite) TestCoinIsValid() {
func (s *coinTestSuite) TestCustomValidation() {
newDnmRegex := `[\x{1F600}-\x{1F6FF}]`
reDnmString := `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}`
sdk.SetCoinDenomRegex(func() string {
return newDnmRegex
})
@@ -126,7 +128,7 @@ func (s *coinTestSuite) TestCustomValidation() {
for i, tc := range cases {
s.Require().Equal(tc.expectPass, tc.coin.IsValid(), "unexpected result for IsValid, tc #%d", i)
}
sdk.SetCoinDenomRegex(sdk.DefaultCoinDenomRegex)
sdk.SetCoinDenomRegex(func() string { return reDnmString })
}
func (s *coinTestSuite) TestCoinsDenoms() {
+60 -6
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"sort"
"strings"
"unicode"
"cosmossdk.io/errors"
"cosmossdk.io/math"
@@ -630,15 +631,24 @@ func (coins DecCoins) Sort() DecCoins {
// ParseDecCoin parses a decimal coin from a string, returning an error if
// invalid. An empty string is considered invalid.
func ParseDecCoin(coinStr string) (coin DecCoin, err error) {
coinStr = strings.TrimSpace(coinStr)
var amountStr, denomStr string
// if custom parsing has not been set, use default coin regex
if reDecCoin == nil {
amountStr, denomStr, err = ParseDecAmount(coinStr)
if err != nil {
return DecCoin{}, err
}
} else {
coinStr = strings.TrimSpace(coinStr)
matches := reDecCoin.FindStringSubmatch(coinStr)
if matches == nil {
return DecCoin{}, fmt.Errorf("invalid decimal coin expression: %s", coinStr)
matches := reDecCoin.FindStringSubmatch(coinStr)
if matches == nil {
return DecCoin{}, fmt.Errorf("invalid decimal coin expression: %s", coinStr)
}
amountStr, denomStr = matches[1], matches[2]
}
amountStr, denomStr := matches[1], matches[2]
amount, err := math.LegacyNewDecFromStr(amountStr)
if err != nil {
return DecCoin{}, errors.Wrap(err, fmt.Sprintf("failed to parse decimal coin amount: %s", amountStr))
@@ -651,6 +661,50 @@ func ParseDecCoin(coinStr string) (coin DecCoin, err error) {
return NewDecCoinFromDec(denomStr, amount), nil
}
// ParseDecAmount parses the given string into amount, denomination.
func ParseDecAmount(coinStr string) (string, string, error) {
var amountRune, denomRune []rune
// Indicates the start of denom parsing
seenLetter := false
// Indicates we're currently parsing the amount
parsingAmount := true
for _, r := range strings.TrimSpace(coinStr) {
if parsingAmount {
if unicode.IsDigit(r) || r == '.' {
amountRune = append(amountRune, r)
} else if unicode.IsSpace(r) { // if space is seen, indicates that we have finished parsing amount
parsingAmount = false
} else if unicode.IsLetter(r) { // if letter is seen, indicates that it is the start of denom
parsingAmount = false
seenLetter = true
denomRune = append(denomRune, r)
} else { // Invalid character encountered in amount part
return "", "", fmt.Errorf("invalid character in coin string: %s", string(r))
}
} else if !seenLetter { // This logic flow is for skipping spaces between amount and denomination
if unicode.IsLetter(r) {
seenLetter = true
denomRune = append(denomRune, r)
} else if !unicode.IsSpace(r) {
// Invalid character before denomination starts
return "", "", fmt.Errorf("invalid start of denomination: %s", string(r))
}
} else {
// Parsing the denomination
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '/' || r == ':' || r == '.' || r == '_' || r == '-' {
denomRune = append(denomRune, r)
} else {
// Invalid character encountered in denomination part
return "", "", fmt.Errorf("invalid character in denomination: %s", string(r))
}
}
}
return string(amountRune), string(denomRune), nil
}
// ParseDecCoins will parse out a list of decimal coins separated by commas. If the parsing is successuful,
// the provided coins will be sanitized by removing zero coins and sorting the coin set. Lastly
// a validation of the coin set is executed. If the check passes, ParseDecCoins will return the sanitized coins.
+3
View File
@@ -377,6 +377,9 @@ func (s *decCoinTestSuite) TestParseDecCoins() {
}{
{"", nil, false},
{"4stake", sdk.DecCoins{sdk.NewDecCoinFromDec("stake", math.LegacyNewDecFromInt(math.NewInt(4)))}, false},
{"5.5atom", sdk.DecCoins{
sdk.NewDecCoinFromDec("atom", math.LegacyNewDecWithPrec(5500000000000000000, math.LegacyPrecision)),
}, false},
{"5.5atom,4stake", sdk.DecCoins{
sdk.NewDecCoinFromDec("atom", math.LegacyNewDecWithPrec(5500000000000000000, math.LegacyPrecision)),
sdk.NewDecCoinFromDec("stake", math.LegacyNewDec(4)),