feat(x/bank): Replace regex parsing of denom validation to generated parsing (#19511)
This commit is contained in:
parent
4edf6b2a85
commit
a1e3a85ae1
@ -11,6 +11,7 @@ run:
|
||||
- ".*\\.pb\\.gw\\.go$"
|
||||
- ".*\\.pulsar\\.go$"
|
||||
- crypto/keys/secp256k1/internal/*
|
||||
- types/coin_regex.go
|
||||
|
||||
build-tags:
|
||||
- e2e
|
||||
|
||||
@ -42,6 +42,7 @@ Every module contains its own CHANGELOG.md. Please refer to the module you are i
|
||||
|
||||
### Features
|
||||
|
||||
* (types) [#19511](https://github.com/cosmos/cosmos-sdk/pull/19511) Replace regex parsing of denom validation to direct matching methods.
|
||||
* (runtime) [#19571](https://github.com/cosmos/cosmos-sdk/pull/19571) Implement `core/router.Service` it in runtime. This service is present in all modules (when using depinject).
|
||||
* (types) [#19164](https://github.com/cosmos/cosmos-sdk/pull/19164) Add a ValueCodec for the math.Uint type that can be used in collections maps.
|
||||
* (types) [#19281](https://github.com/cosmos/cosmos-sdk/pull/19281) Added a new method, `IsGT`, for `types.Coin`. This method is used to check if a `types.Coin` is greater than another `types.Coin`.
|
||||
|
||||
@ -33,7 +33,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
denomRegex = sdk.DefaultCoinDenomRegex()
|
||||
denomRegex = `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}`
|
||||
addr1 = sdk.MustAccAddressFromBech32("cosmos139f7kncmglres2nf3h4hc4tade85ekfr8sulz5")
|
||||
coin1 = sdk.NewCoin("denom", math.NewInt(10))
|
||||
metadataAtom = banktypes.Metadata{
|
||||
|
||||
@ -859,9 +859,10 @@ func TestGRPCRedelegations(t *testing.T) {
|
||||
func TestGRPCParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
f := initDeterministicFixture(t)
|
||||
coinDenomRegex := `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}`
|
||||
|
||||
rapid.Check(t, func(rt *rapid.T) {
|
||||
bondDenom := rapid.StringMatching(sdk.DefaultCoinDenomRegex()).Draw(rt, "bond-denom")
|
||||
bondDenom := rapid.StringMatching(coinDenomRegex).Draw(rt, "bond-denom")
|
||||
params := stakingtypes.Params{
|
||||
BondDenom: bondDenom,
|
||||
UnbondingTime: durationGenerator().Draw(rt, "duration"),
|
||||
|
||||
@ -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
types/coin_regex.go
Normal file
173
types/coin_regex.go
Normal 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
types/coin_regex.rl
Normal file
40
types/coin_regex.rl
Normal 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
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user