diff --git a/.gitattributes b/.gitattributes index adc4144ffa3..13825940056 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,7 @@ go.sum linguist-generated text gnovm/stdlibs/generated.go linguist-generated gnovm/tests/stdlibs/generated.go linguist-generated +*.gen.gno linguist-generated +*.gen_test.gno linguist-generated +*.gen.go linguist-generated +*.gen_test.go linguist-generated \ No newline at end of file diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 6a6d6e02653..5d606a2a663 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -71,12 +71,20 @@ jobs: - run: make lint -C ./examples # TODO: consider running lint on every other directories, maybe in "warning" mode? # TODO: track coverage + fmt: name: Run gno fmt on examples uses: ./.github/workflows/gnofmt_template.yml with: path: "examples/..." + generate: + name: Check generated files are up to date + uses: ./.github/workflows/build_template.yml + with: + modulepath: "examples" + go-version: "1.22.x" + mod-tidy: strategy: fail-fast: false diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index 52e1e23f27b..92d8494fa40 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -82,6 +82,7 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.7.2 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect go.etcd.io/bbolt v1.3.11 // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index e1792734b95..3f22e4f2f00 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -3,8 +3,10 @@ dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -91,6 +93,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -225,10 +229,13 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.2 h1:NjGd7lO7zrUn/A7eKwn5PEOt4ONYGqpxSEeZuduvgxc= github.com/yuin/goldmark v1.7.2/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index fa9e2d11e29..12a88490515 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -489,6 +489,8 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) // Speed up stdlib loading after first start (saves about 2-3 seconds on each reload). nodeConfig.CacheStdlibLoad = true nodeConfig.Genesis.ConsensusParams.Block.MaxGas = n.config.MaxGasPerBlock + // Genesis verification is always false with Gnodev + nodeConfig.SkipGenesisVerification = true // recoverFromError handles panics and converts them to errors. recoverFromError := func() { diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages.go b/contribs/gnogenesis/internal/txs/txs_add_packages.go index cf863c72116..0ab5724154e 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_packages.go +++ b/contribs/gnogenesis/internal/txs/txs_add_packages.go @@ -5,8 +5,9 @@ import ( "errors" "flag" "fmt" + "os" - "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" @@ -15,28 +16,45 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" ) -var ( - errInvalidPackageDir = errors.New("invalid package directory") - errInvalidDeployerAddr = errors.New("invalid deployer address") +const ( + defaultAccount_Name = "test1" + defaultAccount_Address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + defaultAccount_Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" + defaultAccount_publicKey = "gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0skzdkmzu0r9h6gny6eg8c9dc303xrrudee6z4he4y7cs5rnjwmyf40yaj" ) +var errInvalidPackageDir = errors.New("invalid package directory") + // Keep in sync with gno.land/cmd/start.go -var ( - defaultCreator = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 - genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) -) +var genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) type addPkgCfg struct { - txsCfg *txsCfg - deployerAddress string + txsCfg *txsCfg + keyName string + gnoHome string // default GNOHOME env var, just here to ease testing with parallel tests + insecurePasswordStdin bool } func (c *addPkgCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( - &c.deployerAddress, - "deployer-address", - defaultCreator.String(), - "the address that will be used to deploy the package", + &c.keyName, + "key-name", + "", + "The package deployer key name or address contained on gnokey", + ) + + fs.StringVar( + &c.gnoHome, + "gno-home", + os.Getenv("GNOHOME"), + "the gno home directory", + ) + + fs.BoolVar( + &c.insecurePasswordStdin, + "insecure-password-stdin", + false, + "the gno home directory", ) } @@ -65,10 +83,15 @@ func execTxsAddPackages( io commands.IO, args []string, ) error { + var ( + keyname = defaultAccount_Name + keybase keys.Keybase + pass string + ) // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.txsCfg.GenesisPath) - if loadErr != nil { - return fmt.Errorf("unable to load genesis, %w", loadErr) + genesis, err := types.GenesisDocFromFile(cfg.txsCfg.GenesisPath) + if err != nil { + return fmt.Errorf("unable to load genesis, %w", err) } // Make sure the package dir is set @@ -76,19 +99,30 @@ func execTxsAddPackages( return errInvalidPackageDir } - var ( - creator = defaultCreator - err error - ) - - // Check if the deployer address is set - if cfg.deployerAddress != defaultCreator.String() { - creator, err = crypto.AddressFromString(cfg.deployerAddress) + if cfg.keyName != "" { + keyname = cfg.keyName + keybase, err = keys.NewKeyBaseFromDir(cfg.gnoHome) + if err != nil { + return fmt.Errorf("unable to load keybase: %w", err) + } + pass, err = io.GetPassword("Enter password.", cfg.insecurePasswordStdin) + if err != nil { + return fmt.Errorf("cannot read password: %w", err) + } + } else { + keybase = keys.NewInMemory() + _, err := keybase.CreateAccount(defaultAccount_Name, defaultAccount_Seed, "", "", 0, 0) if err != nil { - return fmt.Errorf("%w, %w", errInvalidDeployerAddr, err) + return fmt.Errorf("unable to create account: %w", err) } } + info, err := keybase.GetByNameOrAddress(keyname) + if err != nil { + return fmt.Errorf("unable to find key in keybase: %w", err) + } + + creator := info.GetAddress() parsedTxs := make([]gnoland.TxWithMetadata, 0) for _, path := range args { // Generate transactions from the packages (recursively) @@ -97,6 +131,10 @@ func execTxsAddPackages( return fmt.Errorf("unable to load txs from directory, %w", err) } + if err := signTxs(txs, keybase, genesis.ChainID, keyname, pass); err != nil { + return fmt.Errorf("unable to sign txs, %w", err) + } + parsedTxs = append(parsedTxs, txs...) } @@ -117,3 +155,25 @@ func execTxsAddPackages( return nil } + +func signTxs(txs []gnoland.TxWithMetadata, keybase keys.Keybase, chainID, keyname string, password string) error { + for index, tx := range txs { + // Here accountNumber and sequenceNumber are set to 0 because they are considered as 0 on genesis transactions. + signBytes, err := tx.Tx.GetSignBytes(chainID, 0, 0) + if err != nil { + return fmt.Errorf("unable to load txs from directory, %w", err) + } + signature, publicKey, err := keybase.Sign(keyname, password, signBytes) + if err != nil { + return fmt.Errorf("unable sign tx %w", err) + } + txs[index].Tx.Signatures = []std.Signature{ + { + PubKey: publicKey, + Signature: signature, + }, + } + } + + return nil +} diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages_test.go b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go index c3405d6ff8d..38d930401e8 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_packages_test.go +++ b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go @@ -2,9 +2,11 @@ package txs import ( "context" + "encoding/hex" "fmt" "os" "path/filepath" + "strings" "testing" "github.com/gnolang/contribs/gnogenesis/internal/common" @@ -12,6 +14,8 @@ import ( vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" "github.com/gnolang/gno/tm2/pkg/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,6 +23,7 @@ import ( func TestGenesis_Txs_Add_Packages(t *testing.T) { t.Parallel() + const addPkgExpectedSignature = "cfe5a15d8def04cbdaf9d08e2511db7928152b26419c4577cbfa282c83118852411f3de5d045ce934555572c21bda8042ce5c64b793a01748e49cf2cff7c2983" t.Run("invalid genesis file", func(t *testing.T) { t.Parallel() @@ -60,8 +65,10 @@ func TestGenesis_Txs_Add_Packages(t *testing.T) { assert.ErrorContains(t, cmdErr, errInvalidPackageDir.Error()) }) - t.Run("invalid deployer address", func(t *testing.T) { + t.Run("non existent key", func(t *testing.T) { t.Parallel() + keybaseDir := t.TempDir() + keyname := "beep-boop" tempGenesis, cleanup := testutils.NewTestFile(t) t.Cleanup(cleanup) @@ -69,24 +76,36 @@ func TestGenesis_Txs_Add_Packages(t *testing.T) { genesis := common.GetDefaultGenesis() require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + io := commands.NewTestIO() + io.SetIn( + strings.NewReader( + fmt.Sprintf( + "%s\n", + "password", + ), + ), + ) // Create the command - cmd := NewTxsCmd(commands.NewTestIO()) + cmd := NewTxsCmd(io) args := []string{ "add", "packages", "--genesis-path", tempGenesis.Name(), t.TempDir(), // package dir - "--deployer-address", - "beep-boop", // invalid address + "--key-name", + keyname, // non-existent key name + "--gno-home", + keybaseDir, // temporaryDir for keybase + "--insecure-password-stdin", } // Run the command cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorIs(t, cmdErr, errInvalidDeployerAddr) + assert.ErrorContains(t, cmdErr, "Key "+keyname+" not found") }) - t.Run("valid package", func(t *testing.T) { + t.Run("existent key wrong password", func(t *testing.T) { t.Parallel() tempGenesis, cleanup := testutils.NewTestFile(t) @@ -94,32 +113,189 @@ func TestGenesis_Txs_Add_Packages(t *testing.T) { genesis := common.GetDefaultGenesis() require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + // Prepare the package + var ( + packagePath = "gno.land/p/demo/cuttlas" + dir = t.TempDir() + keybaseDir = t.TempDir() + keyname = "beep-boop" + password = "somepass" + ) + createValidFile(t, dir, packagePath) + // Create key + kb, err := keys.NewKeyBaseFromDir(keybaseDir) + require.NoError(t, err) + mnemonic, err := client.GenerateMnemonic(256) + require.NoError(t, err) + _, err = kb.CreateAccount(keyname, mnemonic, "", password+"wrong", 0, 0) + require.NoError(t, err) + + io := commands.NewTestIO() + io.SetIn( + strings.NewReader( + fmt.Sprintf( + "%s\n", + password, + ), + ), + ) + + // Create the command + cmd := NewTxsCmd(io) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + "--key-name", + keyname, // non-existent key name + "--gno-home", + keybaseDir, // temporaryDir for keybase + "--insecure-password-stdin", + dir, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "unable to sign txs") + }) + + t.Run("existent key correct password", func(t *testing.T) { + t.Parallel() + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) // Prepare the package var ( packagePath = "gno.land/p/demo/cuttlas" dir = t.TempDir() + keybaseDir = t.TempDir() + keyname = "beep-boop" + password = "somepass" ) + createValidFile(t, dir, packagePath) + // Create key + kb, err := keys.NewKeyBaseFromDir(keybaseDir) + require.NoError(t, err) + info, err := kb.CreateAccount(keyname, defaultAccount_Seed, "", password, 0, 0) + require.NoError(t, err) - createFile := func(path, data string) { - file, err := os.Create(path) - require.NoError(t, err) + io := commands.NewTestIO() + io.SetIn( + strings.NewReader( + fmt.Sprintf( + "%s\n", + password, + ), + ), + ) - _, err = file.WriteString(data) - require.NoError(t, err) + // Create the command + cmd := NewTxsCmd(io) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + "--key-name", + keyname, // non-existent key name + "--gno-home", + keybaseDir, // temporaryDir for keybase + "--insecure-password-stdin", + dir, } - // Create the gno.mod file - createFile( - filepath.Join(dir, "gno.mod"), - fmt.Sprintf("module %s\n", packagePath), + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) + + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + require.Equal(t, 1, len(state.Txs)) + require.Equal(t, 1, len(state.Txs[0].Tx.Msgs)) + + msgAddPkg, ok := state.Txs[0].Tx.Msgs[0].(vmm.MsgAddPackage) + require.True(t, ok) + require.Equal(t, info.GetPubKey(), state.Txs[0].Tx.Signatures[0].PubKey) + require.Equal(t, addPkgExpectedSignature, hex.EncodeToString(state.Txs[0].Tx.Signatures[0].Signature)) + + assert.Equal(t, packagePath, msgAddPkg.Package.Path) + }) + + t.Run("ok default key", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + // Prepare the package + var ( + packagePath = "gno.land/p/demo/cuttlas" + dir = t.TempDir() + keybaseDir = t.TempDir() ) + createValidFile(t, dir, packagePath) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + "--gno-home", + keybaseDir, // temporaryDir for keybase + dir, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) - // Create a simple main.gno - createFile( - filepath.Join(dir, "main.gno"), - "package cuttlas\n\nfunc Example() string {\nreturn \"Manos arriba!\"\n}", + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + require.Equal(t, 1, len(state.Txs)) + require.Equal(t, 1, len(state.Txs[0].Tx.Msgs)) + + msgAddPkg, ok := state.Txs[0].Tx.Msgs[0].(vmm.MsgAddPackage) + require.True(t, ok) + require.Equal(t, defaultAccount_publicKey, state.Txs[0].Tx.Signatures[0].PubKey.String()) + require.Equal(t, addPkgExpectedSignature, hex.EncodeToString(state.Txs[0].Tx.Signatures[0].Signature)) + + assert.Equal(t, packagePath, msgAddPkg.Package.Path) + }) + + t.Run("valid package", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + // Prepare the package + var ( + packagePath = "gno.land/p/demo/cuttlas" + dir = t.TempDir() ) + createValidFile(t, dir, packagePath) // Create the command cmd := NewTxsCmd(commands.NewTestIO()) @@ -148,7 +324,32 @@ func TestGenesis_Txs_Add_Packages(t *testing.T) { msgAddPkg, ok := state.Txs[0].Tx.Msgs[0].(vmm.MsgAddPackage) require.True(t, ok) + require.Equal(t, defaultAccount_publicKey, state.Txs[0].Tx.Signatures[0].PubKey.String()) + require.Equal(t, addPkgExpectedSignature, hex.EncodeToString(state.Txs[0].Tx.Signatures[0].Signature)) assert.Equal(t, packagePath, msgAddPkg.Package.Path) }) } + +func createValidFile(t *testing.T, dir string, packagePath string) { + t.Helper() + createFile := func(path, data string) { + file, err := os.Create(path) + require.NoError(t, err) + + _, err = file.WriteString(data) + require.NoError(t, err) + } + + // Create the gno.mod file + createFile( + filepath.Join(dir, "gno.mod"), + fmt.Sprintf("module %s\n", packagePath), + ) + + // Create a simple main.gno + createFile( + filepath.Join(dir, "main.gno"), + "package cuttlas\n\nfunc Example() string {\nreturn \"Manos arriba!\"\n}", + ) +} diff --git a/contribs/gnomd/go.mod b/contribs/gnomd/go.mod index 423e4414a79..57c07621324 100644 --- a/contribs/gnomd/go.mod +++ b/contribs/gnomd/go.mod @@ -21,7 +21,7 @@ require ( github.com/mattn/go-isatty v0.0.11 // indirect github.com/mattn/go-runewidth v0.0.12 // indirect github.com/rivo/uniseg v0.1.0 // indirect - golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 // indirect + golang.org/x/image v0.18.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.18.0 // indirect ) diff --git a/contribs/gnomd/go.sum b/contribs/gnomd/go.sum index 0ff70dd99fb..3d4666530b1 100644 --- a/contribs/gnomd/go.sum +++ b/contribs/gnomd/go.sum @@ -55,8 +55,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 h1:gQ6GUSD102fPgli+Yb4cR/cGaHF7tNBt+GYoRCpGC7s= golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= diff --git a/contribs/gnomigrate/internal/txs/txs.go b/contribs/gnomigrate/internal/txs/txs.go index 4c65ca6ef0b..231428d5064 100644 --- a/contribs/gnomigrate/internal/txs/txs.go +++ b/contribs/gnomigrate/internal/txs/txs.go @@ -184,7 +184,7 @@ func processFile(ctx context.Context, io commands.IO, source, destination string continue } - if _, err = outputFile.WriteString(fmt.Sprintf("%s\n", string(marshaledData))); err != nil { + if _, err = fmt.Fprintf(outputFile, "%s\n", marshaledData); err != nil { io.ErrPrintfln("unable to save to output file, %s", err) } } diff --git a/examples/gno.land/p/demo/grc/grc20/token.gno b/examples/gno.land/p/demo/grc/grc20/token.gno index 3ab3abc63a3..4986eaebf04 100644 --- a/examples/gno.land/p/demo/grc/grc20/token.gno +++ b/examples/gno.land/p/demo/grc/grc20/token.gno @@ -1,7 +1,6 @@ package grc20 import ( - "math/overflow" "std" "strconv" @@ -170,17 +169,24 @@ func (led *PrivateLedger) Approve(owner, spender std.Address, amount uint64) err } // Mint increases the total supply of the token and adds the specified amount to the specified address. -func (led *PrivateLedger) Mint(address std.Address, amount uint64) error { +func (led *PrivateLedger) Mint(address std.Address, amount uint64) (err error) { if !address.IsValid() { return ErrInvalidAddress } - // XXX: math/overflow is not supporting uint64. - // This checks prevents overflow but makes the totalSupply limited to a uint63. - sum, ok := overflow.Add64(int64(led.totalSupply), int64(amount)) - if !ok { - return ErrOverflow - } + defer func() { + if r := recover(); r != nil { + if r != "addition overflow" { + panic(r) + } + err = ErrOverflow + } + }() + + // Convert amount and totalSupply to signed integers to enable + // overflow checking (not occuring on unsigned) when computing the sum. + // The maximum value for totalSupply is therefore 1<<63. + sum := int64(led.totalSupply) + int64(amount) led.totalSupply = uint64(sum) currentBalance := led.balanceOf(address) diff --git a/examples/gno.land/p/moul/addrset/addrset.gno b/examples/gno.land/p/moul/addrset/addrset.gno new file mode 100644 index 00000000000..0bb8165f9fe --- /dev/null +++ b/examples/gno.land/p/moul/addrset/addrset.gno @@ -0,0 +1,100 @@ +// Package addrset provides a specialized set data structure for managing unique Gno addresses. +// +// It is built on top of an AVL tree for efficient operations and maintains addresses in sorted order. +// This package is particularly useful when you need to: +// - Track a collection of unique addresses (e.g., for whitelists, participants, etc.) +// - Efficiently check address membership +// - Support pagination when displaying addresses +// +// Example usage: +// +// import ( +// "std" +// "gno.land/p/moul/addrset" +// ) +// +// func MyHandler() { +// // Create a new address set +// var set addrset.Set +// +// // Add some addresses +// addr1 := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") +// addr2 := std.Address("g1sss5g0rkqr88k4u648yd5d3l9t4d8vvqwszqth") +// +// set.Add(addr1) // returns true (newly added) +// set.Add(addr2) // returns true (newly added) +// set.Add(addr1) // returns false (already exists) +// +// // Check membership +// if set.Has(addr1) { +// // addr1 is in the set +// } +// +// // Get size +// size := set.Size() // returns 2 +// +// // Iterate with pagination (10 items per page, starting at offset 0) +// set.IterateByOffset(0, 10, func(addr std.Address) bool { +// // Process addr +// return false // continue iteration +// }) +// +// // Remove an address +// set.Remove(addr1) // returns true (was present) +// set.Remove(addr1) // returns false (not present) +// } +package addrset + +import ( + "std" + + "gno.land/p/demo/avl" +) + +type Set struct { + tree avl.Tree +} + +// Add inserts an address into the set. +// Returns true if the address was newly added, false if it already existed. +func (s *Set) Add(addr std.Address) bool { + return !s.tree.Set(string(addr), nil) +} + +// Remove deletes an address from the set. +// Returns true if the address was found and removed, false if it didn't exist. +func (s *Set) Remove(addr std.Address) bool { + _, removed := s.tree.Remove(string(addr)) + return removed +} + +// Has checks if an address exists in the set. +func (s *Set) Has(addr std.Address) bool { + return s.tree.Has(string(addr)) +} + +// Size returns the number of addresses in the set. +func (s *Set) Size() int { + return s.tree.Size() +} + +// IterateByOffset walks through addresses starting at the given offset. +// The callback should return true to stop iteration. +func (s *Set) IterateByOffset(offset int, count int, cb func(addr std.Address) bool) { + s.tree.IterateByOffset(offset, count, func(key string, _ interface{}) bool { + return cb(std.Address(key)) + }) +} + +// ReverseIterateByOffset walks through addresses in reverse order starting at the given offset. +// The callback should return true to stop iteration. +func (s *Set) ReverseIterateByOffset(offset int, count int, cb func(addr std.Address) bool) { + s.tree.ReverseIterateByOffset(offset, count, func(key string, _ interface{}) bool { + return cb(std.Address(key)) + }) +} + +// Tree returns the underlying AVL tree for advanced usage. +func (s *Set) Tree() avl.ITree { + return &s.tree +} diff --git a/examples/gno.land/p/moul/addrset/addrset_test.gno b/examples/gno.land/p/moul/addrset/addrset_test.gno new file mode 100644 index 00000000000..c3e27eab1df --- /dev/null +++ b/examples/gno.land/p/moul/addrset/addrset_test.gno @@ -0,0 +1,174 @@ +package addrset + +import ( + "std" + "testing" + + "gno.land/p/demo/uassert" +) + +func TestSet(t *testing.T) { + addr1 := std.Address("addr1") + addr2 := std.Address("addr2") + addr3 := std.Address("addr3") + + tests := []struct { + name string + actions func(s *Set) + size int + has map[std.Address]bool + addrs []std.Address // for iteration checks + }{ + { + name: "empty set", + actions: func(s *Set) {}, + size: 0, + has: map[std.Address]bool{addr1: false}, + }, + { + name: "single address", + actions: func(s *Set) { + s.Add(addr1) + }, + size: 1, + has: map[std.Address]bool{ + addr1: true, + addr2: false, + }, + addrs: []std.Address{addr1}, + }, + { + name: "multiple addresses", + actions: func(s *Set) { + s.Add(addr1) + s.Add(addr2) + s.Add(addr3) + }, + size: 3, + has: map[std.Address]bool{ + addr1: true, + addr2: true, + addr3: true, + }, + addrs: []std.Address{addr1, addr2, addr3}, + }, + { + name: "remove address", + actions: func(s *Set) { + s.Add(addr1) + s.Add(addr2) + s.Remove(addr1) + }, + size: 1, + has: map[std.Address]bool{ + addr1: false, + addr2: true, + }, + addrs: []std.Address{addr2}, + }, + { + name: "duplicate adds", + actions: func(s *Set) { + uassert.True(t, s.Add(addr1)) // first add returns true + uassert.False(t, s.Add(addr1)) // second add returns false + uassert.True(t, s.Remove(addr1)) // remove existing returns true + uassert.False(t, s.Remove(addr1)) // remove non-existing returns false + }, + size: 0, + has: map[std.Address]bool{ + addr1: false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var set Set + + // Execute test actions + tt.actions(&set) + + // Check size + uassert.Equal(t, tt.size, set.Size()) + + // Check existence + for addr, expected := range tt.has { + uassert.Equal(t, expected, set.Has(addr)) + } + + // Check iteration if addresses are specified + if tt.addrs != nil { + collected := []std.Address{} + set.IterateByOffset(0, 10, func(addr std.Address) bool { + collected = append(collected, addr) + return false + }) + + // Check length + uassert.Equal(t, len(tt.addrs), len(collected)) + + // Check each address + for i, addr := range tt.addrs { + uassert.Equal(t, addr, collected[i]) + } + } + }) + } +} + +func TestSetIterationLimits(t *testing.T) { + tests := []struct { + name string + addrs []std.Address + offset int + limit int + expected int + }{ + { + name: "zero offset full list", + addrs: []std.Address{"a1", "a2", "a3"}, + offset: 0, + limit: 10, + expected: 3, + }, + { + name: "offset with limit", + addrs: []std.Address{"a1", "a2", "a3", "a4"}, + offset: 1, + limit: 2, + expected: 2, + }, + { + name: "offset beyond size", + addrs: []std.Address{"a1", "a2"}, + offset: 3, + limit: 1, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var set Set + for _, addr := range tt.addrs { + set.Add(addr) + } + + // Test forward iteration + count := 0 + set.IterateByOffset(tt.offset, tt.limit, func(addr std.Address) bool { + count++ + return false + }) + uassert.Equal(t, tt.expected, count) + + // Test reverse iteration + count = 0 + set.ReverseIterateByOffset(tt.offset, tt.limit, func(addr std.Address) bool { + count++ + return false + }) + uassert.Equal(t, tt.expected, count) + }) + } +} diff --git a/examples/gno.land/p/moul/addrset/gno.mod b/examples/gno.land/p/moul/addrset/gno.mod new file mode 100644 index 00000000000..45bb53b399c --- /dev/null +++ b/examples/gno.land/p/moul/addrset/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/addrset diff --git a/examples/gno.land/p/moul/xmath/generate.go b/examples/gno.land/p/moul/xmath/generate.go new file mode 100644 index 00000000000..ad70adb06bd --- /dev/null +++ b/examples/gno.land/p/moul/xmath/generate.go @@ -0,0 +1,3 @@ +package xmath + +//go:generate go run generator.go diff --git a/examples/gno.land/p/moul/xmath/generator.go b/examples/gno.land/p/moul/xmath/generator.go new file mode 100644 index 00000000000..afe5a4341fa --- /dev/null +++ b/examples/gno.land/p/moul/xmath/generator.go @@ -0,0 +1,184 @@ +//go:build ignore + +package main + +import ( + "bytes" + "fmt" + "go/format" + "log" + "os" + "strings" + "text/template" +) + +type Type struct { + Name string + ZeroValue string + Signed bool + Float bool +} + +var types = []Type{ + {"Int8", "0", true, false}, + {"Int16", "0", true, false}, + {"Int32", "0", true, false}, + {"Int64", "0", true, false}, + {"Int", "0", true, false}, + {"Uint8", "0", false, false}, + {"Uint16", "0", false, false}, + {"Uint32", "0", false, false}, + {"Uint64", "0", false, false}, + {"Uint", "0", false, false}, + {"Float32", "0.0", true, true}, + {"Float64", "0.0", true, true}, +} + +const sourceTpl = `// Code generated by generator.go; DO NOT EDIT. +package xmath + +{{ range .Types }} +// {{.Name}} helpers +func Max{{.Name}}(a, b {{.Name | lower}}) {{.Name | lower}} { + if a > b { + return a + } + return b +} + +func Min{{.Name}}(a, b {{.Name | lower}}) {{.Name | lower}} { + if a < b { + return a + } + return b +} + +func Clamp{{.Name}}(value, min, max {{.Name | lower}}) {{.Name | lower}} { + if value < min { + return min + } + if value > max { + return max + } + return value +} +{{if .Signed}} +func Abs{{.Name}}(x {{.Name | lower}}) {{.Name | lower}} { + if x < 0 { + return -x + } + return x +} + +func Sign{{.Name}}(x {{.Name | lower}}) {{.Name | lower}} { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} +{{end}} +{{end}} +` + +const testTpl = `package xmath + +import "testing" + +{{range .Types}} +func Test{{.Name}}Helpers(t *testing.T) { + // Test Max{{.Name}} + if Max{{.Name}}(1, 2) != 2 { + t.Error("Max{{.Name}}(1, 2) should be 2") + } + {{if .Signed}}if Max{{.Name}}(-1, -2) != -1 { + t.Error("Max{{.Name}}(-1, -2) should be -1") + }{{end}} + + // Test Min{{.Name}} + if Min{{.Name}}(1, 2) != 1 { + t.Error("Min{{.Name}}(1, 2) should be 1") + } + {{if .Signed}}if Min{{.Name}}(-1, -2) != -2 { + t.Error("Min{{.Name}}(-1, -2) should be -2") + }{{end}} + + // Test Clamp{{.Name}} + if Clamp{{.Name}}(5, 1, 3) != 3 { + t.Error("Clamp{{.Name}}(5, 1, 3) should be 3") + } + if Clamp{{.Name}}(0, 1, 3) != 1 { + t.Error("Clamp{{.Name}}(0, 1, 3) should be 1") + } + if Clamp{{.Name}}(2, 1, 3) != 2 { + t.Error("Clamp{{.Name}}(2, 1, 3) should be 2") + } + {{if .Signed}} + // Test Abs{{.Name}} + if Abs{{.Name}}(-5) != 5 { + t.Error("Abs{{.Name}}(-5) should be 5") + } + if Abs{{.Name}}(5) != 5 { + t.Error("Abs{{.Name}}(5) should be 5") + } + + // Test Sign{{.Name}} + if Sign{{.Name}}(-5) != -1 { + t.Error("Sign{{.Name}}(-5) should be -1") + } + if Sign{{.Name}}(5) != 1 { + t.Error("Sign{{.Name}}(5) should be 1") + } + if Sign{{.Name}}({{.ZeroValue}}) != 0 { + t.Error("Sign{{.Name}}({{.ZeroValue}}) should be 0") + } + {{end}} +} +{{end}} +` + +func main() { + funcMap := template.FuncMap{ + "lower": strings.ToLower, + } + + // Generate source file + sourceTmpl := template.Must(template.New("source").Funcs(funcMap).Parse(sourceTpl)) + var sourceOut bytes.Buffer + if err := sourceTmpl.Execute(&sourceOut, struct{ Types []Type }{types}); err != nil { + log.Fatal(err) + } + + // Format the generated code + formattedSource, err := format.Source(sourceOut.Bytes()) + if err != nil { + log.Fatal(err) + } + + // Write source file + if err := os.WriteFile("xmath.gen.gno", formattedSource, 0644); err != nil { + log.Fatal(err) + } + + // Generate test file + testTmpl := template.Must(template.New("test").Parse(testTpl)) + var testOut bytes.Buffer + if err := testTmpl.Execute(&testOut, struct{ Types []Type }{types}); err != nil { + log.Fatal(err) + } + + // Format the generated test code + formattedTest, err := format.Source(testOut.Bytes()) + if err != nil { + log.Fatal(err) + } + + // Write test file + if err := os.WriteFile("xmath.gen_test.gno", formattedTest, 0644); err != nil { + log.Fatal(err) + } + + fmt.Println("Generated xmath.gen.gno and xmath.gen_test.gno") +} diff --git a/examples/gno.land/p/moul/xmath/gno.mod b/examples/gno.land/p/moul/xmath/gno.mod new file mode 100644 index 00000000000..63b782c88f2 --- /dev/null +++ b/examples/gno.land/p/moul/xmath/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/xmath diff --git a/examples/gno.land/p/moul/xmath/xmath.gen.gno b/examples/gno.land/p/moul/xmath/xmath.gen.gno new file mode 100644 index 00000000000..266c77e1e84 --- /dev/null +++ b/examples/gno.land/p/moul/xmath/xmath.gen.gno @@ -0,0 +1,421 @@ +// Code generated by generator.go; DO NOT EDIT. +package xmath + +// Int8 helpers +func MaxInt8(a, b int8) int8 { + if a > b { + return a + } + return b +} + +func MinInt8(a, b int8) int8 { + if a < b { + return a + } + return b +} + +func ClampInt8(value, min, max int8) int8 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt8(x int8) int8 { + if x < 0 { + return -x + } + return x +} + +func SignInt8(x int8) int8 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Int16 helpers +func MaxInt16(a, b int16) int16 { + if a > b { + return a + } + return b +} + +func MinInt16(a, b int16) int16 { + if a < b { + return a + } + return b +} + +func ClampInt16(value, min, max int16) int16 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt16(x int16) int16 { + if x < 0 { + return -x + } + return x +} + +func SignInt16(x int16) int16 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Int32 helpers +func MaxInt32(a, b int32) int32 { + if a > b { + return a + } + return b +} + +func MinInt32(a, b int32) int32 { + if a < b { + return a + } + return b +} + +func ClampInt32(value, min, max int32) int32 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt32(x int32) int32 { + if x < 0 { + return -x + } + return x +} + +func SignInt32(x int32) int32 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Int64 helpers +func MaxInt64(a, b int64) int64 { + if a > b { + return a + } + return b +} + +func MinInt64(a, b int64) int64 { + if a < b { + return a + } + return b +} + +func ClampInt64(value, min, max int64) int64 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt64(x int64) int64 { + if x < 0 { + return -x + } + return x +} + +func SignInt64(x int64) int64 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Int helpers +func MaxInt(a, b int) int { + if a > b { + return a + } + return b +} + +func MinInt(a, b int) int { + if a < b { + return a + } + return b +} + +func ClampInt(value, min, max int) int { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt(x int) int { + if x < 0 { + return -x + } + return x +} + +func SignInt(x int) int { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Uint8 helpers +func MaxUint8(a, b uint8) uint8 { + if a > b { + return a + } + return b +} + +func MinUint8(a, b uint8) uint8 { + if a < b { + return a + } + return b +} + +func ClampUint8(value, min, max uint8) uint8 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Uint16 helpers +func MaxUint16(a, b uint16) uint16 { + if a > b { + return a + } + return b +} + +func MinUint16(a, b uint16) uint16 { + if a < b { + return a + } + return b +} + +func ClampUint16(value, min, max uint16) uint16 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Uint32 helpers +func MaxUint32(a, b uint32) uint32 { + if a > b { + return a + } + return b +} + +func MinUint32(a, b uint32) uint32 { + if a < b { + return a + } + return b +} + +func ClampUint32(value, min, max uint32) uint32 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Uint64 helpers +func MaxUint64(a, b uint64) uint64 { + if a > b { + return a + } + return b +} + +func MinUint64(a, b uint64) uint64 { + if a < b { + return a + } + return b +} + +func ClampUint64(value, min, max uint64) uint64 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Uint helpers +func MaxUint(a, b uint) uint { + if a > b { + return a + } + return b +} + +func MinUint(a, b uint) uint { + if a < b { + return a + } + return b +} + +func ClampUint(value, min, max uint) uint { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Float32 helpers +func MaxFloat32(a, b float32) float32 { + if a > b { + return a + } + return b +} + +func MinFloat32(a, b float32) float32 { + if a < b { + return a + } + return b +} + +func ClampFloat32(value, min, max float32) float32 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsFloat32(x float32) float32 { + if x < 0 { + return -x + } + return x +} + +func SignFloat32(x float32) float32 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Float64 helpers +func MaxFloat64(a, b float64) float64 { + if a > b { + return a + } + return b +} + +func MinFloat64(a, b float64) float64 { + if a < b { + return a + } + return b +} + +func ClampFloat64(value, min, max float64) float64 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsFloat64(x float64) float64 { + if x < 0 { + return -x + } + return x +} + +func SignFloat64(x float64) float64 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} diff --git a/examples/gno.land/p/moul/xmath/xmath.gen_test.gno b/examples/gno.land/p/moul/xmath/xmath.gen_test.gno new file mode 100644 index 00000000000..16c80fc983d --- /dev/null +++ b/examples/gno.land/p/moul/xmath/xmath.gen_test.gno @@ -0,0 +1,466 @@ +package xmath + +import "testing" + +func TestInt8Helpers(t *testing.T) { + // Test MaxInt8 + if MaxInt8(1, 2) != 2 { + t.Error("MaxInt8(1, 2) should be 2") + } + if MaxInt8(-1, -2) != -1 { + t.Error("MaxInt8(-1, -2) should be -1") + } + + // Test MinInt8 + if MinInt8(1, 2) != 1 { + t.Error("MinInt8(1, 2) should be 1") + } + if MinInt8(-1, -2) != -2 { + t.Error("MinInt8(-1, -2) should be -2") + } + + // Test ClampInt8 + if ClampInt8(5, 1, 3) != 3 { + t.Error("ClampInt8(5, 1, 3) should be 3") + } + if ClampInt8(0, 1, 3) != 1 { + t.Error("ClampInt8(0, 1, 3) should be 1") + } + if ClampInt8(2, 1, 3) != 2 { + t.Error("ClampInt8(2, 1, 3) should be 2") + } + + // Test AbsInt8 + if AbsInt8(-5) != 5 { + t.Error("AbsInt8(-5) should be 5") + } + if AbsInt8(5) != 5 { + t.Error("AbsInt8(5) should be 5") + } + + // Test SignInt8 + if SignInt8(-5) != -1 { + t.Error("SignInt8(-5) should be -1") + } + if SignInt8(5) != 1 { + t.Error("SignInt8(5) should be 1") + } + if SignInt8(0) != 0 { + t.Error("SignInt8(0) should be 0") + } + +} + +func TestInt16Helpers(t *testing.T) { + // Test MaxInt16 + if MaxInt16(1, 2) != 2 { + t.Error("MaxInt16(1, 2) should be 2") + } + if MaxInt16(-1, -2) != -1 { + t.Error("MaxInt16(-1, -2) should be -1") + } + + // Test MinInt16 + if MinInt16(1, 2) != 1 { + t.Error("MinInt16(1, 2) should be 1") + } + if MinInt16(-1, -2) != -2 { + t.Error("MinInt16(-1, -2) should be -2") + } + + // Test ClampInt16 + if ClampInt16(5, 1, 3) != 3 { + t.Error("ClampInt16(5, 1, 3) should be 3") + } + if ClampInt16(0, 1, 3) != 1 { + t.Error("ClampInt16(0, 1, 3) should be 1") + } + if ClampInt16(2, 1, 3) != 2 { + t.Error("ClampInt16(2, 1, 3) should be 2") + } + + // Test AbsInt16 + if AbsInt16(-5) != 5 { + t.Error("AbsInt16(-5) should be 5") + } + if AbsInt16(5) != 5 { + t.Error("AbsInt16(5) should be 5") + } + + // Test SignInt16 + if SignInt16(-5) != -1 { + t.Error("SignInt16(-5) should be -1") + } + if SignInt16(5) != 1 { + t.Error("SignInt16(5) should be 1") + } + if SignInt16(0) != 0 { + t.Error("SignInt16(0) should be 0") + } + +} + +func TestInt32Helpers(t *testing.T) { + // Test MaxInt32 + if MaxInt32(1, 2) != 2 { + t.Error("MaxInt32(1, 2) should be 2") + } + if MaxInt32(-1, -2) != -1 { + t.Error("MaxInt32(-1, -2) should be -1") + } + + // Test MinInt32 + if MinInt32(1, 2) != 1 { + t.Error("MinInt32(1, 2) should be 1") + } + if MinInt32(-1, -2) != -2 { + t.Error("MinInt32(-1, -2) should be -2") + } + + // Test ClampInt32 + if ClampInt32(5, 1, 3) != 3 { + t.Error("ClampInt32(5, 1, 3) should be 3") + } + if ClampInt32(0, 1, 3) != 1 { + t.Error("ClampInt32(0, 1, 3) should be 1") + } + if ClampInt32(2, 1, 3) != 2 { + t.Error("ClampInt32(2, 1, 3) should be 2") + } + + // Test AbsInt32 + if AbsInt32(-5) != 5 { + t.Error("AbsInt32(-5) should be 5") + } + if AbsInt32(5) != 5 { + t.Error("AbsInt32(5) should be 5") + } + + // Test SignInt32 + if SignInt32(-5) != -1 { + t.Error("SignInt32(-5) should be -1") + } + if SignInt32(5) != 1 { + t.Error("SignInt32(5) should be 1") + } + if SignInt32(0) != 0 { + t.Error("SignInt32(0) should be 0") + } + +} + +func TestInt64Helpers(t *testing.T) { + // Test MaxInt64 + if MaxInt64(1, 2) != 2 { + t.Error("MaxInt64(1, 2) should be 2") + } + if MaxInt64(-1, -2) != -1 { + t.Error("MaxInt64(-1, -2) should be -1") + } + + // Test MinInt64 + if MinInt64(1, 2) != 1 { + t.Error("MinInt64(1, 2) should be 1") + } + if MinInt64(-1, -2) != -2 { + t.Error("MinInt64(-1, -2) should be -2") + } + + // Test ClampInt64 + if ClampInt64(5, 1, 3) != 3 { + t.Error("ClampInt64(5, 1, 3) should be 3") + } + if ClampInt64(0, 1, 3) != 1 { + t.Error("ClampInt64(0, 1, 3) should be 1") + } + if ClampInt64(2, 1, 3) != 2 { + t.Error("ClampInt64(2, 1, 3) should be 2") + } + + // Test AbsInt64 + if AbsInt64(-5) != 5 { + t.Error("AbsInt64(-5) should be 5") + } + if AbsInt64(5) != 5 { + t.Error("AbsInt64(5) should be 5") + } + + // Test SignInt64 + if SignInt64(-5) != -1 { + t.Error("SignInt64(-5) should be -1") + } + if SignInt64(5) != 1 { + t.Error("SignInt64(5) should be 1") + } + if SignInt64(0) != 0 { + t.Error("SignInt64(0) should be 0") + } + +} + +func TestIntHelpers(t *testing.T) { + // Test MaxInt + if MaxInt(1, 2) != 2 { + t.Error("MaxInt(1, 2) should be 2") + } + if MaxInt(-1, -2) != -1 { + t.Error("MaxInt(-1, -2) should be -1") + } + + // Test MinInt + if MinInt(1, 2) != 1 { + t.Error("MinInt(1, 2) should be 1") + } + if MinInt(-1, -2) != -2 { + t.Error("MinInt(-1, -2) should be -2") + } + + // Test ClampInt + if ClampInt(5, 1, 3) != 3 { + t.Error("ClampInt(5, 1, 3) should be 3") + } + if ClampInt(0, 1, 3) != 1 { + t.Error("ClampInt(0, 1, 3) should be 1") + } + if ClampInt(2, 1, 3) != 2 { + t.Error("ClampInt(2, 1, 3) should be 2") + } + + // Test AbsInt + if AbsInt(-5) != 5 { + t.Error("AbsInt(-5) should be 5") + } + if AbsInt(5) != 5 { + t.Error("AbsInt(5) should be 5") + } + + // Test SignInt + if SignInt(-5) != -1 { + t.Error("SignInt(-5) should be -1") + } + if SignInt(5) != 1 { + t.Error("SignInt(5) should be 1") + } + if SignInt(0) != 0 { + t.Error("SignInt(0) should be 0") + } + +} + +func TestUint8Helpers(t *testing.T) { + // Test MaxUint8 + if MaxUint8(1, 2) != 2 { + t.Error("MaxUint8(1, 2) should be 2") + } + + // Test MinUint8 + if MinUint8(1, 2) != 1 { + t.Error("MinUint8(1, 2) should be 1") + } + + // Test ClampUint8 + if ClampUint8(5, 1, 3) != 3 { + t.Error("ClampUint8(5, 1, 3) should be 3") + } + if ClampUint8(0, 1, 3) != 1 { + t.Error("ClampUint8(0, 1, 3) should be 1") + } + if ClampUint8(2, 1, 3) != 2 { + t.Error("ClampUint8(2, 1, 3) should be 2") + } + +} + +func TestUint16Helpers(t *testing.T) { + // Test MaxUint16 + if MaxUint16(1, 2) != 2 { + t.Error("MaxUint16(1, 2) should be 2") + } + + // Test MinUint16 + if MinUint16(1, 2) != 1 { + t.Error("MinUint16(1, 2) should be 1") + } + + // Test ClampUint16 + if ClampUint16(5, 1, 3) != 3 { + t.Error("ClampUint16(5, 1, 3) should be 3") + } + if ClampUint16(0, 1, 3) != 1 { + t.Error("ClampUint16(0, 1, 3) should be 1") + } + if ClampUint16(2, 1, 3) != 2 { + t.Error("ClampUint16(2, 1, 3) should be 2") + } + +} + +func TestUint32Helpers(t *testing.T) { + // Test MaxUint32 + if MaxUint32(1, 2) != 2 { + t.Error("MaxUint32(1, 2) should be 2") + } + + // Test MinUint32 + if MinUint32(1, 2) != 1 { + t.Error("MinUint32(1, 2) should be 1") + } + + // Test ClampUint32 + if ClampUint32(5, 1, 3) != 3 { + t.Error("ClampUint32(5, 1, 3) should be 3") + } + if ClampUint32(0, 1, 3) != 1 { + t.Error("ClampUint32(0, 1, 3) should be 1") + } + if ClampUint32(2, 1, 3) != 2 { + t.Error("ClampUint32(2, 1, 3) should be 2") + } + +} + +func TestUint64Helpers(t *testing.T) { + // Test MaxUint64 + if MaxUint64(1, 2) != 2 { + t.Error("MaxUint64(1, 2) should be 2") + } + + // Test MinUint64 + if MinUint64(1, 2) != 1 { + t.Error("MinUint64(1, 2) should be 1") + } + + // Test ClampUint64 + if ClampUint64(5, 1, 3) != 3 { + t.Error("ClampUint64(5, 1, 3) should be 3") + } + if ClampUint64(0, 1, 3) != 1 { + t.Error("ClampUint64(0, 1, 3) should be 1") + } + if ClampUint64(2, 1, 3) != 2 { + t.Error("ClampUint64(2, 1, 3) should be 2") + } + +} + +func TestUintHelpers(t *testing.T) { + // Test MaxUint + if MaxUint(1, 2) != 2 { + t.Error("MaxUint(1, 2) should be 2") + } + + // Test MinUint + if MinUint(1, 2) != 1 { + t.Error("MinUint(1, 2) should be 1") + } + + // Test ClampUint + if ClampUint(5, 1, 3) != 3 { + t.Error("ClampUint(5, 1, 3) should be 3") + } + if ClampUint(0, 1, 3) != 1 { + t.Error("ClampUint(0, 1, 3) should be 1") + } + if ClampUint(2, 1, 3) != 2 { + t.Error("ClampUint(2, 1, 3) should be 2") + } + +} + +func TestFloat32Helpers(t *testing.T) { + // Test MaxFloat32 + if MaxFloat32(1, 2) != 2 { + t.Error("MaxFloat32(1, 2) should be 2") + } + if MaxFloat32(-1, -2) != -1 { + t.Error("MaxFloat32(-1, -2) should be -1") + } + + // Test MinFloat32 + if MinFloat32(1, 2) != 1 { + t.Error("MinFloat32(1, 2) should be 1") + } + if MinFloat32(-1, -2) != -2 { + t.Error("MinFloat32(-1, -2) should be -2") + } + + // Test ClampFloat32 + if ClampFloat32(5, 1, 3) != 3 { + t.Error("ClampFloat32(5, 1, 3) should be 3") + } + if ClampFloat32(0, 1, 3) != 1 { + t.Error("ClampFloat32(0, 1, 3) should be 1") + } + if ClampFloat32(2, 1, 3) != 2 { + t.Error("ClampFloat32(2, 1, 3) should be 2") + } + + // Test AbsFloat32 + if AbsFloat32(-5) != 5 { + t.Error("AbsFloat32(-5) should be 5") + } + if AbsFloat32(5) != 5 { + t.Error("AbsFloat32(5) should be 5") + } + + // Test SignFloat32 + if SignFloat32(-5) != -1 { + t.Error("SignFloat32(-5) should be -1") + } + if SignFloat32(5) != 1 { + t.Error("SignFloat32(5) should be 1") + } + if SignFloat32(0.0) != 0 { + t.Error("SignFloat32(0.0) should be 0") + } + +} + +func TestFloat64Helpers(t *testing.T) { + // Test MaxFloat64 + if MaxFloat64(1, 2) != 2 { + t.Error("MaxFloat64(1, 2) should be 2") + } + if MaxFloat64(-1, -2) != -1 { + t.Error("MaxFloat64(-1, -2) should be -1") + } + + // Test MinFloat64 + if MinFloat64(1, 2) != 1 { + t.Error("MinFloat64(1, 2) should be 1") + } + if MinFloat64(-1, -2) != -2 { + t.Error("MinFloat64(-1, -2) should be -2") + } + + // Test ClampFloat64 + if ClampFloat64(5, 1, 3) != 3 { + t.Error("ClampFloat64(5, 1, 3) should be 3") + } + if ClampFloat64(0, 1, 3) != 1 { + t.Error("ClampFloat64(0, 1, 3) should be 1") + } + if ClampFloat64(2, 1, 3) != 2 { + t.Error("ClampFloat64(2, 1, 3) should be 2") + } + + // Test AbsFloat64 + if AbsFloat64(-5) != 5 { + t.Error("AbsFloat64(-5) should be 5") + } + if AbsFloat64(5) != 5 { + t.Error("AbsFloat64(5) should be 5") + } + + // Test SignFloat64 + if SignFloat64(-5) != -1 { + t.Error("SignFloat64(-5) should be -1") + } + if SignFloat64(5) != 1 { + t.Error("SignFloat64(5) should be 1") + } + if SignFloat64(0.0) != 0 { + t.Error("SignFloat64(0.0) should be 0") + } + +} diff --git a/examples/gno.land/r/demo/btree_dao/btree_dao.gno b/examples/gno.land/r/demo/btree_dao/btree_dao.gno new file mode 100644 index 00000000000..c90742eb29b --- /dev/null +++ b/examples/gno.land/r/demo/btree_dao/btree_dao.gno @@ -0,0 +1,209 @@ +package btree_dao + +import ( + "errors" + "std" + "strings" + "time" + + "gno.land/p/demo/btree" + "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" +) + +// RegistrationDetails holds the details of a user's registration in the BTree DAO. +// It stores the user's address, registration time, their B-Tree if they planted one, +// and their NFT ID. +type RegistrationDetails struct { + Address std.Address + RegTime time.Time + UserBTree *btree.BTree + NFTID string +} + +// Less implements the btree.Record interface for RegistrationDetails. +// It compares two RegistrationDetails based on their registration time. +// Returns true if the current registration time is before the other registration time. +func (rd *RegistrationDetails) Less(than btree.Record) bool { + other := than.(*RegistrationDetails) + return rd.RegTime.Before(other.RegTime) +} + +var ( + dao = grc721.NewBasicNFT("BTree DAO", "BTDAO") + tokenID = 0 + members = btree.New() +) + +// PlantTree allows a user to plant their B-Tree in the DAO forest. +// It mints an NFT to the user and registers their tree in the DAO. +// Returns an error if the tree is already planted, empty, or if NFT minting fails. +func PlantTree(userBTree *btree.BTree) error { + return plantImpl(userBTree, "") +} + +// PlantSeed allows a user to register as a seed in the DAO with a message. +// It mints an NFT to the user and registers them as a seed member. +// Returns an error if the message is empty or if NFT minting fails. +func PlantSeed(message string) error { + return plantImpl(nil, message) +} + +// plantImpl is the internal implementation that handles both tree planting and seed registration. +// For tree planting (userBTree != nil), it verifies the tree isn't already planted and isn't empty. +// For seed planting (userBTree == nil), it verifies the seed message isn't empty. +// In both cases, it mints an NFT to the user and adds their registration details to the members tree. +// Returns an error if any validation fails or if NFT minting fails. +func plantImpl(userBTree *btree.BTree, seedMessage string) error { + // Get the caller's address + userAddress := std.GetOrigCaller() + + var nftID string + var regDetails *RegistrationDetails + + if userBTree != nil { + // Handle tree planting + var treeExists bool + members.Ascend(func(record btree.Record) bool { + regDetails := record.(*RegistrationDetails) + if regDetails.UserBTree == userBTree { + treeExists = true + return false + } + return true + }) + if treeExists { + return errors.New("tree is already planted in the forest") + } + + if userBTree.Len() == 0 { + return errors.New("cannot plant an empty tree") + } + + nftID = ufmt.Sprintf("%d", tokenID) + regDetails = &RegistrationDetails{ + Address: userAddress, + RegTime: time.Now(), + UserBTree: userBTree, + NFTID: nftID, + } + } else { + // Handle seed planting + if seedMessage == "" { + return errors.New("seed message cannot be empty") + } + nftID = "seed_" + ufmt.Sprintf("%d", tokenID) + regDetails = &RegistrationDetails{ + Address: userAddress, + RegTime: time.Now(), + UserBTree: nil, + NFTID: nftID, + } + } + + // Mint an NFT to the user + err := dao.Mint(userAddress, grc721.TokenID(nftID)) + if err != nil { + return err + } + + members.Insert(regDetails) + tokenID++ + return nil +} + +// Render generates a Markdown representation of the DAO members. +// It displays: +// - Total number of NFTs minted +// - Total number of members +// - Size of the biggest planted tree +// - The first 3 members (OGs) +// - The latest 10 members +// Each member entry includes their address and owned NFTs (π³ for trees, π± for seeds). +// The path parameter is currently unused. +// Returns a formatted Markdown string. +func Render(path string) string { + var latestMembers []string + var ogMembers []string + + // Get total size and first member + totalSize := members.Len() + biggestTree := 0 + if maxMember := members.Max(); maxMember != nil { + if userBTree := maxMember.(*RegistrationDetails).UserBTree; userBTree != nil { + biggestTree = userBTree.Len() + } + } + + // Collect the latest 10 members + members.Descend(func(record btree.Record) bool { + if len(latestMembers) < 10 { + regDetails := record.(*RegistrationDetails) + addr := regDetails.Address + nftList := "" + balance, err := dao.BalanceOf(addr) + if err == nil && balance > 0 { + nftList = " (NFTs: " + for i := uint64(0); i < balance; i++ { + if i > 0 { + nftList += ", " + } + if regDetails.UserBTree == nil { + nftList += "π±#" + regDetails.NFTID + } else { + nftList += "π³#" + regDetails.NFTID + } + } + nftList += ")" + } + latestMembers = append(latestMembers, string(addr)+nftList) + return true + } + return false + }) + + // Collect the first 3 members (OGs) + members.Ascend(func(record btree.Record) bool { + if len(ogMembers) < 3 { + regDetails := record.(*RegistrationDetails) + addr := regDetails.Address + nftList := "" + balance, err := dao.BalanceOf(addr) + if err == nil && balance > 0 { + nftList = " (NFTs: " + for i := uint64(0); i < balance; i++ { + if i > 0 { + nftList += ", " + } + if regDetails.UserBTree == nil { + nftList += "π±#" + regDetails.NFTID + } else { + nftList += "π³#" + regDetails.NFTID + } + } + nftList += ")" + } + ogMembers = append(ogMembers, string(addr)+nftList) + return true + } + return false + }) + + var sb strings.Builder + + sb.WriteString(md.H1("B-Tree DAO Members")) + sb.WriteString(md.H2("Total NFTs Minted")) + sb.WriteString(ufmt.Sprintf("Total NFTs minted: %d\n\n", dao.TokenCount())) + sb.WriteString(md.H2("Member Stats")) + sb.WriteString(ufmt.Sprintf("Total members: %d\n", totalSize)) + if biggestTree > 0 { + sb.WriteString(ufmt.Sprintf("Biggest tree size: %d\n", biggestTree)) + } + sb.WriteString(md.H2("OG Members")) + sb.WriteString(md.BulletList(ogMembers)) + sb.WriteString(md.H2("Latest Members")) + sb.WriteString(md.BulletList(latestMembers)) + + return sb.String() +} diff --git a/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno b/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno new file mode 100644 index 00000000000..0514f52f7b4 --- /dev/null +++ b/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno @@ -0,0 +1,97 @@ +package btree_dao + +import ( + "std" + "strings" + "testing" + "time" + + "gno.land/p/demo/btree" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func setupTest() { + std.TestSetOrigCaller(std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y")) + members = btree.New() +} + +type TestElement struct { + value int +} + +func (te *TestElement) Less(than btree.Record) bool { + return te.value < than.(*TestElement).value +} + +func TestPlantTree(t *testing.T) { + setupTest() + + tree := btree.New() + elements := []int{30, 10, 50, 20, 40} + for _, val := range elements { + tree.Insert(&TestElement{value: val}) + } + + err := PlantTree(tree) + urequire.NoError(t, err) + + found := false + members.Ascend(func(record btree.Record) bool { + regDetails := record.(*RegistrationDetails) + if regDetails.UserBTree == tree { + found = true + return false + } + return true + }) + uassert.True(t, found) + + err = PlantTree(tree) + uassert.Error(t, err) + + emptyTree := btree.New() + err = PlantTree(emptyTree) + uassert.Error(t, err) +} + +func TestPlantSeed(t *testing.T) { + setupTest() + + err := PlantSeed("Hello DAO!") + urequire.NoError(t, err) + + found := false + members.Ascend(func(record btree.Record) bool { + regDetails := record.(*RegistrationDetails) + if regDetails.UserBTree == nil { + found = true + uassert.NotEmpty(t, regDetails.NFTID) + uassert.True(t, strings.Contains(regDetails.NFTID, "seed_")) + return false + } + return true + }) + uassert.True(t, found) + + err = PlantSeed("") + uassert.Error(t, err) +} + +func TestRegistrationDetailsOrdering(t *testing.T) { + setupTest() + + rd1 := &RegistrationDetails{ + Address: std.Address("test1"), + RegTime: time.Now(), + NFTID: "0", + } + rd2 := &RegistrationDetails{ + Address: std.Address("test2"), + RegTime: time.Now().Add(time.Hour), + NFTID: "1", + } + + uassert.True(t, rd1.Less(rd2)) + uassert.False(t, rd2.Less(rd1)) +} diff --git a/examples/gno.land/r/demo/btree_dao/gno.mod b/examples/gno.land/r/demo/btree_dao/gno.mod new file mode 100644 index 00000000000..01b99acc300 --- /dev/null +++ b/examples/gno.land/r/demo/btree_dao/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/btree_dao diff --git a/examples/gno.land/r/stefann/home/home.gno b/examples/gno.land/r/stefann/home/home.gno index 9586f377311..f54721ce37c 100644 --- a/examples/gno.land/r/stefann/home/home.gno +++ b/examples/gno.land/r/stefann/home/home.gno @@ -8,6 +8,8 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" + "gno.land/r/demo/users" + "gno.land/r/leon/hof" "gno.land/r/stefann/registry" ) @@ -23,7 +25,6 @@ type Sponsor struct { } type Profile struct { - pfp string aboutMe []string } @@ -49,15 +50,15 @@ var ( func init() { owner = ownable.NewWithAddress(registry.MainAddr()) + hof.Register() profile = Profile{ - pfp: "https://i.ibb.co/Bc5YNCx/DSC-0095a.jpg", aboutMe: []string{ - `### About Me`, - `Hey there! Iβm Stefan, a student of Computer Science. Iβm all about exploring and adventure β whether itβs diving into the latest tech or discovering a new city, Iβm always up for the challenge!`, + `## About Me`, + `### Hey there! Iβm Stefan, a student of Computer Science. Iβm all about exploring and adventure β whether itβs diving into the latest tech or discovering a new city, Iβm always up for the challenge!`, - `### Contributions`, - `I'm just getting started, but you can follow my journey through gno.land right [here](https://github.com/gnolang/hackerspace/issues/94) π`, + `## Contributions`, + `### I'm just getting started, but you can follow my journey through gno.land right [here](https://github.com/gnolang/hackerspace/issues/94) π`, }, } @@ -83,7 +84,7 @@ func init() { } sponsorship = Sponsorship{ - maxSponsors: 5, + maxSponsors: 3, sponsors: avl.NewTree(), DonationsCount: 0, sponsorsCount: 0, @@ -106,11 +107,6 @@ func UpdateJarLink(newLink string) { travel.jarLink = newLink } -func UpdatePFP(url string) { - owner.AssertCallerIsOwner() - profile.pfp = url -} - func UpdateAboutMe(aboutMeStr string) { owner.AssertCallerIsOwner() profile.aboutMe = strings.Split(aboutMeStr, "|") @@ -203,46 +199,27 @@ func Render(path string) string { } func renderAboutMe() string { - out := "
No sponsors yet. Be the first to tip the jar!
` + "\n" + return out + "No sponsors yet. Be the first to tip the jar!\n" } topSponsors := GetTopSponsors() @@ -266,38 +259,30 @@ func renderSponsors() string { numSponsors = sponsorship.maxSponsors } - out += `')
- }
- l := n.Lines().Len()
- for i := 0; i < l; i++ {
- line := n.Lines().At(i)
- r.Writer.RawWrite(w, line.Value(source))
- }
- if r.WrapperRenderer != nil {
- r.WrapperRenderer(w, c, false)
- } else {
- _, _ = w.WriteString("
\n")
- }
- return ast.WalkContinue, nil
-}
-
-type highlighting struct {
- options []Option
-}
-
-// Highlighting is a goldmark.Extender implementation.
-var Highlighting = &highlighting{
- options: []Option{},
-}
-
-// NewHighlighting returns a new extension with given options.
-func NewHighlighting(opts ...Option) goldmark.Extender {
- return &highlighting{
- options: opts,
- }
-}
-
-// Extend implements goldmark.Extender.
-func (e *highlighting) Extend(m goldmark.Markdown) {
- m.Renderer().AddOptions(renderer.WithNodeRenderers(
- util.Prioritized(NewHTMLRenderer(e.options...), 200),
- ))
-}
diff --git a/gno.land/pkg/gnoweb/markdown/highlighting_test.go b/gno.land/pkg/gnoweb/markdown/highlighting_test.go
deleted file mode 100644
index 25bc4fedd61..00000000000
--- a/gno.land/pkg/gnoweb/markdown/highlighting_test.go
+++ /dev/null
@@ -1,568 +0,0 @@
-// This file was copied from https://github.com/yuin/goldmark-highlighting
-
-package markdown
-
-import (
- "bytes"
- "fmt"
- "strings"
- "testing"
-
- "github.com/alecthomas/chroma/v2"
- chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
- "github.com/yuin/goldmark"
- "github.com/yuin/goldmark/testutil"
- "github.com/yuin/goldmark/util"
-)
-
-func TestHighlighting(t *testing.T) {
- var css bytes.Buffer
- markdown := goldmark.New(
- goldmark.WithExtensions(
- NewHighlighting(
- WithStyle("monokai"),
- WithCSSWriter(&css),
- WithFormatOptions(
- chromahtml.WithClasses(true),
- chromahtml.WithLineNumbers(false),
- ),
- WithWrapperRenderer(func(w util.BufWriter, c CodeBlockContext, entering bool) {
- _, ok := c.Language()
- if entering {
- if !ok {
- w.WriteString("")
- return
- }
- w.WriteString(``)
- } else {
- if !ok {
- w.WriteString("")
- return
- }
- w.WriteString(``)
- }
- }),
- WithCodeBlockOptions(func(c CodeBlockContext) []chromahtml.Option {
- if language, ok := c.Language(); ok {
- // Turn on line numbers for Go only.
- if string(language) == "go" {
- return []chromahtml.Option{
- chromahtml.WithLineNumbers(true),
- }
- }
- }
- return nil
- }),
- ),
- ),
- )
- var buffer bytes.Buffer
- if err := markdown.Convert([]byte(`
-Title
-=======
-`+"``` go\n"+`func main() {
- fmt.Println("ok")
-}
-`+"```"+`
-`), &buffer); err != nil {
- t.Fatal(err)
- }
-
- if strings.TrimSpace(buffer.String()) != strings.TrimSpace(`
-Title
-1func main() {
-2 fmt.Println("ok")
-3}
-
-`) {
- t.Errorf("failed to render HTML\n%s", buffer.String())
- }
-
- expected := strings.TrimSpace(`/* Background */ .bg { color: #f8f8f2; background-color: #272822; }
-/* PreWrapper */ .chroma { color: #f8f8f2; background-color: #272822; }
-/* LineNumbers targeted by URL anchor */ .chroma .ln:target { color: #f8f8f2; background-color: #3c3d38 }
-/* LineNumbersTable targeted by URL anchor */ .chroma .lnt:target { color: #f8f8f2; background-color: #3c3d38 }
-/* Error */ .chroma .err { color: #960050; background-color: #1e0010 }
-/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
-/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
-/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
-/* LineHighlight */ .chroma .hl { background-color: #3c3d38 }
-/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
-/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
-/* Line */ .chroma .line { display: flex; }
-/* Keyword */ .chroma .k { color: #66d9ef }
-/* KeywordConstant */ .chroma .kc { color: #66d9ef }
-/* KeywordDeclaration */ .chroma .kd { color: #66d9ef }
-/* KeywordNamespace */ .chroma .kn { color: #f92672 }
-/* KeywordPseudo */ .chroma .kp { color: #66d9ef }
-/* KeywordReserved */ .chroma .kr { color: #66d9ef }
-/* KeywordType */ .chroma .kt { color: #66d9ef }
-/* NameAttribute */ .chroma .na { color: #a6e22e }
-/* NameClass */ .chroma .nc { color: #a6e22e }
-/* NameConstant */ .chroma .no { color: #66d9ef }
-/* NameDecorator */ .chroma .nd { color: #a6e22e }
-/* NameException */ .chroma .ne { color: #a6e22e }
-/* NameFunction */ .chroma .nf { color: #a6e22e }
-/* NameOther */ .chroma .nx { color: #a6e22e }
-/* NameTag */ .chroma .nt { color: #f92672 }
-/* Literal */ .chroma .l { color: #ae81ff }
-/* LiteralDate */ .chroma .ld { color: #e6db74 }
-/* LiteralString */ .chroma .s { color: #e6db74 }
-/* LiteralStringAffix */ .chroma .sa { color: #e6db74 }
-/* LiteralStringBacktick */ .chroma .sb { color: #e6db74 }
-/* LiteralStringChar */ .chroma .sc { color: #e6db74 }
-/* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 }
-/* LiteralStringDoc */ .chroma .sd { color: #e6db74 }
-/* LiteralStringDouble */ .chroma .s2 { color: #e6db74 }
-/* LiteralStringEscape */ .chroma .se { color: #ae81ff }
-/* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 }
-/* LiteralStringInterpol */ .chroma .si { color: #e6db74 }
-/* LiteralStringOther */ .chroma .sx { color: #e6db74 }
-/* LiteralStringRegex */ .chroma .sr { color: #e6db74 }
-/* LiteralStringSingle */ .chroma .s1 { color: #e6db74 }
-/* LiteralStringSymbol */ .chroma .ss { color: #e6db74 }
-/* LiteralNumber */ .chroma .m { color: #ae81ff }
-/* LiteralNumberBin */ .chroma .mb { color: #ae81ff }
-/* LiteralNumberFloat */ .chroma .mf { color: #ae81ff }
-/* LiteralNumberHex */ .chroma .mh { color: #ae81ff }
-/* LiteralNumberInteger */ .chroma .mi { color: #ae81ff }
-/* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff }
-/* LiteralNumberOct */ .chroma .mo { color: #ae81ff }
-/* Operator */ .chroma .o { color: #f92672 }
-/* OperatorWord */ .chroma .ow { color: #f92672 }
-/* Comment */ .chroma .c { color: #75715e }
-/* CommentHashbang */ .chroma .ch { color: #75715e }
-/* CommentMultiline */ .chroma .cm { color: #75715e }
-/* CommentSingle */ .chroma .c1 { color: #75715e }
-/* CommentSpecial */ .chroma .cs { color: #75715e }
-/* CommentPreproc */ .chroma .cp { color: #75715e }
-/* CommentPreprocFile */ .chroma .cpf { color: #75715e }
-/* GenericDeleted */ .chroma .gd { color: #f92672 }
-/* GenericEmph */ .chroma .ge { font-style: italic }
-/* GenericInserted */ .chroma .gi { color: #a6e22e }
-/* GenericStrong */ .chroma .gs { font-weight: bold }
-/* GenericSubheading */ .chroma .gu { color: #75715e }`)
-
- gotten := strings.TrimSpace(css.String())
-
- if expected != gotten {
- diff := testutil.DiffPretty([]byte(expected), []byte(gotten))
- t.Errorf("incorrect CSS.\n%s", string(diff))
- }
-}
-
-func TestHighlighting2(t *testing.T) {
- markdown := goldmark.New(
- goldmark.WithExtensions(
- Highlighting,
- ),
- )
- var buffer bytes.Buffer
- if err := markdown.Convert([]byte(`
-Title
-=======
-`+"```"+`
-func main() {
- fmt.Println("ok")
-}
-`+"```"+`
-`), &buffer); err != nil {
- t.Fatal(err)
- }
-
- if strings.TrimSpace(buffer.String()) != strings.TrimSpace(`
-Title
-func main() {
- fmt.Println("ok")
-}
-
-`) {
- t.Error("failed to render HTML")
- }
-}
-
-func TestHighlighting3(t *testing.T) {
- markdown := goldmark.New(
- goldmark.WithExtensions(
- Highlighting,
- ),
- )
- var buffer bytes.Buffer
- if err := markdown.Convert([]byte(`
-Title
-=======
-
-`+"```"+`cpp {hl_lines=[1,2]}
-#include
-int main() {
- std::cout<< "hello" << std::endl;
-}
-`+"```"+`
-`), &buffer); err != nil {
- t.Fatal(err)
- }
- if strings.TrimSpace(buffer.String()) != strings.TrimSpace(`
-Title
-#include <iostream>
-int main() {
- std::cout<< "hello" << std::endl;
-}
-
-`) {
- t.Errorf("failed to render HTML:\n%s", buffer.String())
- }
-}
-
-func TestHighlightingCustom(t *testing.T) {
- custom := chroma.MustNewStyle("custom", chroma.StyleEntries{
- chroma.Background: "#cccccc bg:#1d1d1d",
- chroma.Comment: "#999999",
- chroma.CommentSpecial: "#cd0000",
- chroma.Keyword: "#cc99cd",
- chroma.KeywordDeclaration: "#cc99cd",
- chroma.KeywordNamespace: "#cc99cd",
- chroma.KeywordType: "#cc99cd",
- chroma.Operator: "#67cdcc",
- chroma.OperatorWord: "#cdcd00",
- chroma.NameClass: "#f08d49",
- chroma.NameBuiltin: "#f08d49",
- chroma.NameFunction: "#f08d49",
- chroma.NameException: "bold #666699",
- chroma.NameVariable: "#00cdcd",
- chroma.LiteralString: "#7ec699",
- chroma.LiteralNumber: "#f08d49",
- chroma.LiteralStringBoolean: "#f08d49",
- chroma.GenericHeading: "bold #000080",
- chroma.GenericSubheading: "bold #800080",
- chroma.GenericDeleted: "#e2777a",
- chroma.GenericInserted: "#cc99cd",
- chroma.GenericError: "#e2777a",
- chroma.GenericEmph: "italic",
- chroma.GenericStrong: "bold",
- chroma.GenericPrompt: "bold #000080",
- chroma.GenericOutput: "#888",
- chroma.GenericTraceback: "#04D",
- chroma.GenericUnderline: "underline",
- chroma.Error: "border:#e2777a",
- })
-
- var css bytes.Buffer
- markdown := goldmark.New(
- goldmark.WithExtensions(
- NewHighlighting(
- WithStyle("monokai"), // to make sure it is overrided even if present
- WithCustomStyle(custom),
- WithCSSWriter(&css),
- WithFormatOptions(
- chromahtml.WithClasses(true),
- chromahtml.WithLineNumbers(false),
- ),
- WithWrapperRenderer(func(w util.BufWriter, c CodeBlockContext, entering bool) {
- _, ok := c.Language()
- if entering {
- if !ok {
- w.WriteString("")
- return
- }
- w.WriteString(``)
- } else {
- if !ok {
- w.WriteString("")
- return
- }
- w.WriteString(``)
- }
- }),
- WithCodeBlockOptions(func(c CodeBlockContext) []chromahtml.Option {
- if language, ok := c.Language(); ok {
- // Turn on line numbers for Go only.
- if string(language) == "go" {
- return []chromahtml.Option{
- chromahtml.WithLineNumbers(true),
- }
- }
- }
- return nil
- }),
- ),
- ),
- )
- var buffer bytes.Buffer
- if err := markdown.Convert([]byte(`
-Title
-=======
-`+"``` go\n"+`func main() {
- fmt.Println("ok")
-}
-`+"```"+`
-`), &buffer); err != nil {
- t.Fatal(err)
- }
-
- if strings.TrimSpace(buffer.String()) != strings.TrimSpace(`
-Title
-1func main() {
-2 fmt.Println("ok")
-3}
-
-`) {
- t.Error("failed to render HTML", buffer.String())
- }
-
- expected := strings.TrimSpace(`/* Background */ .bg { color: #cccccc; background-color: #1d1d1d; }
-/* PreWrapper */ .chroma { color: #cccccc; background-color: #1d1d1d; }
-/* LineNumbers targeted by URL anchor */ .chroma .ln:target { color: #cccccc; background-color: #333333 }
-/* LineNumbersTable targeted by URL anchor */ .chroma .lnt:target { color: #cccccc; background-color: #333333 }
-/* Error */ .chroma .err { }
-/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
-/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
-/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
-/* LineHighlight */ .chroma .hl { background-color: #333333 }
-/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #666666 }
-/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #666666 }
-/* Line */ .chroma .line { display: flex; }
-/* Keyword */ .chroma .k { color: #cc99cd }
-/* KeywordConstant */ .chroma .kc { color: #cc99cd }
-/* KeywordDeclaration */ .chroma .kd { color: #cc99cd }
-/* KeywordNamespace */ .chroma .kn { color: #cc99cd }
-/* KeywordPseudo */ .chroma .kp { color: #cc99cd }
-/* KeywordReserved */ .chroma .kr { color: #cc99cd }
-/* KeywordType */ .chroma .kt { color: #cc99cd }
-/* NameBuiltin */ .chroma .nb { color: #f08d49 }
-/* NameClass */ .chroma .nc { color: #f08d49 }
-/* NameException */ .chroma .ne { color: #666699; font-weight: bold }
-/* NameFunction */ .chroma .nf { color: #f08d49 }
-/* NameVariable */ .chroma .nv { color: #00cdcd }
-/* LiteralString */ .chroma .s { color: #7ec699 }
-/* LiteralStringAffix */ .chroma .sa { color: #7ec699 }
-/* LiteralStringBacktick */ .chroma .sb { color: #7ec699 }
-/* LiteralStringChar */ .chroma .sc { color: #7ec699 }
-/* LiteralStringDelimiter */ .chroma .dl { color: #7ec699 }
-/* LiteralStringDoc */ .chroma .sd { color: #7ec699 }
-/* LiteralStringDouble */ .chroma .s2 { color: #7ec699 }
-/* LiteralStringEscape */ .chroma .se { color: #7ec699 }
-/* LiteralStringHeredoc */ .chroma .sh { color: #7ec699 }
-/* LiteralStringInterpol */ .chroma .si { color: #7ec699 }
-/* LiteralStringOther */ .chroma .sx { color: #7ec699 }
-/* LiteralStringRegex */ .chroma .sr { color: #7ec699 }
-/* LiteralStringSingle */ .chroma .s1 { color: #7ec699 }
-/* LiteralStringSymbol */ .chroma .ss { color: #7ec699 }
-/* LiteralNumber */ .chroma .m { color: #f08d49 }
-/* LiteralNumberBin */ .chroma .mb { color: #f08d49 }
-/* LiteralNumberFloat */ .chroma .mf { color: #f08d49 }
-/* LiteralNumberHex */ .chroma .mh { color: #f08d49 }
-/* LiteralNumberInteger */ .chroma .mi { color: #f08d49 }
-/* LiteralNumberIntegerLong */ .chroma .il { color: #f08d49 }
-/* LiteralNumberOct */ .chroma .mo { color: #f08d49 }
-/* Operator */ .chroma .o { color: #67cdcc }
-/* OperatorWord */ .chroma .ow { color: #cdcd00 }
-/* Comment */ .chroma .c { color: #999999 }
-/* CommentHashbang */ .chroma .ch { color: #999999 }
-/* CommentMultiline */ .chroma .cm { color: #999999 }
-/* CommentSingle */ .chroma .c1 { color: #999999 }
-/* CommentSpecial */ .chroma .cs { color: #cd0000 }
-/* CommentPreproc */ .chroma .cp { color: #999999 }
-/* CommentPreprocFile */ .chroma .cpf { color: #999999 }
-/* GenericDeleted */ .chroma .gd { color: #e2777a }
-/* GenericEmph */ .chroma .ge { font-style: italic }
-/* GenericError */ .chroma .gr { color: #e2777a }
-/* GenericHeading */ .chroma .gh { color: #000080; font-weight: bold }
-/* GenericInserted */ .chroma .gi { color: #cc99cd }
-/* GenericOutput */ .chroma .go { color: #888888 }
-/* GenericPrompt */ .chroma .gp { color: #000080; font-weight: bold }
-/* GenericStrong */ .chroma .gs { font-weight: bold }
-/* GenericSubheading */ .chroma .gu { color: #800080; font-weight: bold }
-/* GenericTraceback */ .chroma .gt { color: #0044dd }
-/* GenericUnderline */ .chroma .gl { text-decoration: underline }`)
-
- gotten := strings.TrimSpace(css.String())
-
- if expected != gotten {
- diff := testutil.DiffPretty([]byte(expected), []byte(gotten))
- t.Errorf("incorrect CSS.\n%s", string(diff))
- }
-}
-
-func TestHighlightingHlLines(t *testing.T) {
- markdown := goldmark.New(
- goldmark.WithExtensions(
- NewHighlighting(
- WithFormatOptions(
- chromahtml.WithClasses(true),
- ),
- ),
- ),
- )
-
- for i, test := range []struct {
- attributes string
- expect []int
- }{
- {`hl_lines=["2"]`, []int{2}},
- {`hl_lines=["2-3",5],linenostart=5`, []int{2, 3, 5}},
- {`hl_lines=["2-3"]`, []int{2, 3}},
- {`hl_lines=["2-3",5],linenostart="5"`, []int{2, 3}}, // linenostart must be a number. string values are ignored
- } {
- t.Run(fmt.Sprint(i), func(t *testing.T) {
- var buffer bytes.Buffer
- codeBlock := fmt.Sprintf(`bash {%s}
-LINE1
-LINE2
-LINE3
-LINE4
-LINE5
-LINE6
-LINE7
-LINE8
-`, test.attributes)
-
- if err := markdown.Convert([]byte(`
-`+"```"+codeBlock+"```"+`
-`), &buffer); err != nil {
- t.Fatal(err)
- }
-
- for _, line := range test.expect {
- expectStr := fmt.Sprintf("LINE%d\n", line)
- if !strings.Contains(buffer.String(), expectStr) {
- t.Fatal("got\n", buffer.String(), "\nexpected\n", expectStr)
- }
- }
- })
- }
-}
-
-type nopPreWrapper struct{}
-
-// Start is called to write a start element.
-func (nopPreWrapper) Start(code bool, styleAttr string) string { return "" }
-
-// End is called to write the end
element.
-func (nopPreWrapper) End(code bool) string { return "" }
-
-func TestHighlightingLinenos(t *testing.T) {
- outputLineNumbersInTable := `
-
-1
-
-
-LINE1
-
-`
-
- for i, test := range []struct {
- attributes string
- lineNumbers bool
- lineNumbersInTable bool
- expect string
- }{
- {`linenos=true`, false, false, `1LINE1
-`},
- {`linenos=false`, false, false, `LINE1
-`},
- {``, true, false, `1LINE1
-`},
- {``, true, true, outputLineNumbersInTable},
- {`linenos=inline`, true, true, `1LINE1
-`},
- {`linenos=foo`, false, false, `1LINE1
-`},
- {`linenos=table`, false, false, outputLineNumbersInTable},
- } {
- t.Run(fmt.Sprint(i), func(t *testing.T) {
- markdown := goldmark.New(
- goldmark.WithExtensions(
- NewHighlighting(
- WithFormatOptions(
- chromahtml.WithLineNumbers(test.lineNumbers),
- chromahtml.LineNumbersInTable(test.lineNumbersInTable),
- chromahtml.WithPreWrapper(nopPreWrapper{}),
- chromahtml.WithClasses(true),
- ),
- ),
- ),
- )
-
- var buffer bytes.Buffer
- codeBlock := fmt.Sprintf(`bash {%s}
-LINE1
-`, test.attributes)
-
- content := "```" + codeBlock + "```"
-
- if err := markdown.Convert([]byte(content), &buffer); err != nil {
- t.Fatal(err)
- }
-
- s := strings.TrimSpace(buffer.String())
-
- if s != test.expect {
- t.Fatal("got\n", s, "\nexpected\n", test.expect)
- }
- })
- }
-}
-
-func TestHighlightingGuessLanguage(t *testing.T) {
- markdown := goldmark.New(
- goldmark.WithExtensions(
- NewHighlighting(
- WithGuessLanguage(true),
- WithFormatOptions(
- chromahtml.WithClasses(true),
- chromahtml.WithLineNumbers(true),
- ),
- ),
- ),
- )
- var buffer bytes.Buffer
- if err := markdown.Convert([]byte("```"+`
-LINE
-`+"```"), &buffer); err != nil {
- t.Fatal(err)
- }
- if strings.TrimSpace(buffer.String()) != strings.TrimSpace(`
-1LINE
-
-`) {
- t.Errorf("render mismatch, got\n%s", buffer.String())
- }
-}
-
-func TestCoalesceNeeded(t *testing.T) {
- markdown := goldmark.New(
- goldmark.WithExtensions(
- NewHighlighting(
- // WithGuessLanguage(true),
- WithFormatOptions(
- chromahtml.WithClasses(true),
- chromahtml.WithLineNumbers(true),
- ),
- ),
- ),
- )
- var buffer bytes.Buffer
- if err := markdown.Convert([]byte("```http"+`
-GET /foo HTTP/1.1
-Content-Type: application/json
-User-Agent: foo
-
-{
- "hello": "world"
-}
-`+"```"), &buffer); err != nil {
- t.Fatal(err)
- }
- if strings.TrimSpace(buffer.String()) != strings.TrimSpace(`
-1GET /foo HTTP/1.1
-2Content-Type: application/json
-3User-Agent: foo
-4
-5{
-6 "hello": "world"
-7}
-
-`) {
- t.Errorf("render mismatch, got\n%s", buffer.String())
- }
-}
diff --git a/gno.land/pkg/gnoweb/public/styles.css b/gno.land/pkg/gnoweb/public/styles.css
index a1d7860c63e..a6771695454 100644
--- a/gno.land/pkg/gnoweb/public/styles.css
+++ b/gno.land/pkg/gnoweb/public/styles.css
@@ -1,3 +1,3 @@
@font-face{font-family:Roboto;font-style:normal;font-weight:900;font-display:swap;src:url(fonts/roboto/roboto-mono-normal.woff2) format("woff2"),url(fonts/roboto/roboto-mono-normal.woff) format("woff")}@font-face{font-family:Inter var;font-weight:100 900;font-display:block;font-style:oblique 0deg 10deg;src:url(fonts/intervar/Intervar.woff2) format("woff2")}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }
-/*! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #bdbdbd}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#7c7c7c}input::placeholder,textarea::placeholder{opacity:1;color:#7c7c7c}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));font-family:Inter var,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji,sans-serif;font-size:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;font-smoothing:antialiased;font-variant-ligatures:contextual common-ligatures;font-kerning:normal;text-rendering:optimizeLegibility}svg{max-height:100%;max-width:100%}form{margin-top:0;margin-bottom:0}.realm-content{overflow-wrap:break-word;padding-top:2.5rem;font-size:1rem}.realm-content>:first-child{margin-top:0!important}.realm-content a{font-weight:500;--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.realm-content a:hover{text-decoration-line:underline}.realm-content h1,.realm-content h2,.realm-content h3,.realm-content h4{margin-top:3rem;line-height:1.25;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content h2,.realm-content h2 *{font-weight:700}.realm-content h3,.realm-content h3 *,.realm-content h4,.realm-content h4 *{font-weight:600}.realm-content h1+h2,.realm-content h2+h3,.realm-content h3+h4{margin-top:1rem}.realm-content h1{font-size:2.375rem;font-weight:700}.realm-content h2{font-size:1.5rem}.realm-content h3{margin-top:2.5rem;font-size:1.25rem}.realm-content h3,.realm-content h4{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content h4{margin-top:1.5rem;margin-bottom:1.5rem;font-size:1.125rem;font-weight:500}.realm-content p{margin-top:1.25rem;margin-bottom:1.25rem}.realm-content strong{font-weight:700;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content strong *{font-weight:700}.realm-content em{font-style:oblique 10deg}.realm-content blockquote{margin-top:1rem;margin-bottom:1rem;border-left-width:4px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-style:oblique 10deg}.realm-content ol,.realm-content ul{margin-top:1.5rem;margin-bottom:1.5rem;padding-left:1rem}.realm-content ol li,.realm-content ul li{margin-bottom:.5rem}.realm-content img{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-content figure{margin-top:1.5rem;margin-bottom:1.5rem;text-align:center}.realm-content figcaption{font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content :not(pre)>code{border-radius:.25rem;background-color:rgb(226 226 226/var(--tw-bg-opacity));padding:.125rem .25rem;font-size:.96em}.realm-content :not(pre)>code,.realm-content pre{--tw-bg-opacity:1;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content pre{overflow-x:auto;border-radius:.375rem;background-color:rgb(240 240 240/var(--tw-bg-opacity));padding:1rem}.realm-content hr{margin-top:2.5rem;margin-bottom:2.5rem;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.realm-content table{margin-top:2rem;margin-bottom:2rem;width:100%;border-collapse:collapse}.realm-content td,.realm-content th{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}.realm-content th{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));font-weight:700}.realm-content caption{margin-top:.5rem;text-align:left;font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content q{margin-top:1.5rem;margin-bottom:1.5rem;border-left-width:4px;--tw-border-opacity:1;border-left-color:rgb(204 204 204/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(85 85 85/var(--tw-text-opacity));font-style:oblique 10deg;quotes:"β" "β" "β" "β"}.realm-content q:after,.realm-content q:before{margin-right:.25rem;font-size:1.5rem;--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity));content:open-quote;vertical-align:-.4rem}.realm-content q:after{content:close-quote}.realm-content q:before{content:open-quote}.realm-content q:after{content:close-quote}.realm-content ol ol,.realm-content ol ul,.realm-content ul ol,.realm-content ul ul{margin-top:.75rem;margin-bottom:.5rem;padding-left:1rem}.realm-content ul{list-style-type:disc}.realm-content ol{list-style-type:decimal}.realm-content table th:first-child,.realm-content td:first-child{padding-left:0}.realm-content table th:last-child,.realm-content td:last-child{padding-right:0}.realm-content abbr[title]{cursor:help;border-bottom-width:1px;border-style:dotted}.realm-content details{margin-top:1.25rem;margin-bottom:1.25rem}.realm-content summary{cursor:pointer;font-weight:700}.realm-content a code{color:inherit}.realm-content video{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-content math{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content small{font-size:.875rem}.realm-content del{text-decoration-line:line-through}.realm-content sub{vertical-align:sub;font-size:.75rem}.realm-content sup{vertical-align:super;font-size:.75rem}.realm-content button,.realm-content input{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}main :is(h1,h2,h3,h4){scroll-margin-top:6rem}::-moz-selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}::selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.sidemenu .peer:checked+label>svg{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.toc-expend-btn:has(#toc-expend:checked)+nav{display:block}.toc-expend-btn:has(#toc-expend:checked) .toc-expend-btn_ico{--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.main-header:has(#sidemenu-docs:checked)+main #sidebar #sidebar-docs,.main-header:has(#sidemenu-meta:checked)+main #sidebar #sidebar-meta,.main-header:has(#sidemenu-source:checked)+main #sidebar #sidebar-source,.main-header:has(#sidemenu-summary:checked)+main #sidebar #sidebar-summary{display:block}@media (min-width:40rem){:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .main-navigation,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main .realm-content{grid-column:span 6/span 6}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .sidemenu,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar{grid-column:span 4/span 4}}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar:before{position:absolute;top:0;left:-1.75rem;z-index:-1;display:block;height:100%;width:50vw;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}main :is(.source-code)>pre{overflow:scroll;border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;padding:1rem .25rem;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-size:.875rem}@media (min-width:40rem){main :is(.source-code)>pre{padding:2rem .75rem;font-size:1rem}}main .realm-content>pre a:hover{text-decoration-line:none}main :is(.realm-content,.source-code)>pre .chroma-ln:target{background-color:transparent!important}main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target) .chroma-cl,main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl{border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(226 226 226/var(--tw-bg-opacity))!important}main :is(.realm-content,.source-code)>pre .chroma-ln{scroll-margin-top:6rem}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.bottom-1{bottom:.25rem}.left-0{left:0}.right-2{right:.5rem}.right-3{right:.75rem}.top-0{top:0}.top-1\/2{top:50%}.top-14{top:3.5rem}.top-2{top:.5rem}.z-max{z-index:9999}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-3{grid-column:span 3/span 3}.col-span-7{grid-column:span 7/span 7}.row-span-1{grid-row:span 1/span 1}.row-start-1{grid-row-start:1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.mr-10{margin-right:2.5rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.max-h-screen{max-height:100vh}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-full{width:100%}.min-w-2{min-width:.5rem}.min-w-48{min-width:12rem}.max-w-screen-max{max-width:98.75rem}.shrink-0{flex-shrink:0}.grow-\[2\]{flex-grow:2}.-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-flow-dense{grid-auto-flow:dense}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.375rem}.rounded-sm{border-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.bg-light{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.font-mono{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.text-100{font-size:.875rem}.text-200{font-size:1rem}.text-50{font-size:.75rem}.text-600{font-size:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-tight{line-height:1.25}.text-gray-300{--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(124 124 124/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(19 19 19/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.outline-none{outline:2px solid transparent;outline-offset:2px}.text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.\*\:pl-0>*{padding-left:0}.before\:px-\[0\.18rem\]:before{content:var(--tw-content);padding-left:.18rem;padding-right:.18rem}.before\:text-gray-300:before{content:var(--tw-content);--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.before\:content-\[\'\/\'\]:before{--tw-content:"/";content:var(--tw-content)}.before\:content-\[\'open\'\]:before{--tw-content:"open";content:var(--tw-content)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-0:after{content:var(--tw-content);left:0}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:rounded-t-sm:after{content:var(--tw-content);border-top-left-radius:.25rem;border-top-right-radius:.25rem}.after\:bg-green-600:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.first\:border-t:first-child{border-top-width:1px}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.hover\:text-green-600:hover{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.hover\:text-light:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.focus\:border-l-gray-300:focus{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-gray-300{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-l-gray-300{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group.is-active .group-\[\.is-active\]\:text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.peer:checked~.peer-checked\:before\:content-\[\'close\'\]:before{--tw-content:"close";content:var(--tw-content)}.peer:focus-within~.peer-focus-within\:hidden{display:none}.has-\[ul\:empty\]\:hidden:has(ul:empty){display:none}.has-\[\:focus-within\]\:border-gray-300:has(:focus-within){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.has-\[\:focus\]\:border-gray-300:has(:focus){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}@media (min-width:30rem){.sm\:gap-6{gap:1.5rem}}@media (min-width:40rem){.md\:col-span-3{grid-column:span 3/span 3}.md\:mb-0{margin-bottom:0}.md\:h-4{height:1rem}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:pb-0{padding-bottom:0}}@media (min-width:51.25rem){.lg\:order-2{order:2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-start-1{grid-row-start:1}.lg\:row-start-2{grid-row-start:2}.lg\:mb-4{margin-bottom:1rem}.lg\:mt-0{margin-top:0}.lg\:mt-10{margin-top:2.5rem}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:justify-start{justify-content:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.lg\:border-none{border-style:none}.lg\:bg-transparent{background-color:transparent}.lg\:p-0{padding:0}.lg\:px-0{padding-left:0;padding-right:0}.lg\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.lg\:pb-28{padding-bottom:7rem}.lg\:pt-2{padding-top:.5rem}.lg\:text-200{font-size:1rem}.lg\:font-semibold{font-weight:600}.lg\:hover\:bg-transparent:hover{background-color:transparent}}@media (min-width:63.75rem){.xl\:inline{display:inline}.xl\:hidden{display:none}.xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:gap-20{gap:5rem}.xl\:gap-6{gap:1.5rem}.xl\:pt-0{padding-top:0}}@media (min-width:85.375rem){.xxl\:inline-block{display:inline-block}.xxl\:h-4{height:1rem}.xxl\:w-4{width:1rem}.xxl\:gap-20{gap:5rem}.xxl\:gap-x-32{-moz-column-gap:8rem;column-gap:8rem}.xxl\:pr-1{padding-right:.25rem}}
\ No newline at end of file
+/*! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #bdbdbd}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#7c7c7c}input::placeholder,textarea::placeholder{opacity:1;color:#7c7c7c}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));font-family:Inter var,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji,sans-serif;font-size:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;font-smoothing:antialiased;font-variant-ligatures:contextual common-ligatures;font-kerning:normal;text-rendering:optimizeLegibility}svg{max-height:100%;max-width:100%}form{margin-top:0;margin-bottom:0}.realm-content{overflow-wrap:break-word;padding-top:2.5rem;font-size:1rem}.realm-content>:first-child{margin-top:0!important}.realm-content a{font-weight:500;--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.realm-content a:hover{text-decoration-line:underline}.realm-content h1,.realm-content h2,.realm-content h3,.realm-content h4{margin-top:3rem;line-height:1.25;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content h2,.realm-content h2 *{font-weight:700}.realm-content h3,.realm-content h3 *,.realm-content h4,.realm-content h4 *{font-weight:600}.realm-content h1+h2,.realm-content h2+h3,.realm-content h3+h4{margin-top:1rem}.realm-content h1{font-size:2.375rem;font-weight:700}.realm-content h2{font-size:1.5rem}.realm-content h3{margin-top:2.5rem;font-size:1.25rem}.realm-content h3,.realm-content h4{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content h4{margin-top:1.5rem;margin-bottom:1.5rem;font-size:1.125rem;font-weight:500}.realm-content p{margin-top:1.25rem;margin-bottom:1.25rem}.realm-content strong{font-weight:700;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content strong *{font-weight:700}.realm-content em{font-style:oblique 10deg}.realm-content blockquote{margin-top:1rem;margin-bottom:1rem;border-left-width:4px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-style:oblique 10deg}.realm-content ol,.realm-content ul{margin-top:1.5rem;margin-bottom:1.5rem;padding-left:1rem}.realm-content ol li,.realm-content ul li{margin-bottom:.5rem}.realm-content img{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-content figure{margin-top:1.5rem;margin-bottom:1.5rem;text-align:center}.realm-content figcaption{font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content :not(pre)>code{border-radius:.25rem;background-color:rgb(226 226 226/var(--tw-bg-opacity));padding:.125rem .25rem;font-size:.96em}.realm-content :not(pre)>code,.realm-content pre{--tw-bg-opacity:1;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content pre{overflow-x:auto;border-radius:.375rem;background-color:rgb(240 240 240/var(--tw-bg-opacity));padding:1rem}.realm-content hr{margin-top:2.5rem;margin-bottom:2.5rem;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.realm-content table{margin-top:2rem;margin-bottom:2rem;width:100%;border-collapse:collapse}.realm-content td,.realm-content th{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}.realm-content th{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));font-weight:700}.realm-content caption{margin-top:.5rem;text-align:left;font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content q{margin-top:1.5rem;margin-bottom:1.5rem;border-left-width:4px;--tw-border-opacity:1;border-left-color:rgb(204 204 204/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(85 85 85/var(--tw-text-opacity));font-style:oblique 10deg;quotes:"β" "β" "β" "β"}.realm-content q:after,.realm-content q:before{margin-right:.25rem;font-size:1.5rem;--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity));content:open-quote;vertical-align:-.4rem}.realm-content q:after{content:close-quote}.realm-content q:before{content:open-quote}.realm-content q:after{content:close-quote}.realm-content ol ol,.realm-content ol ul,.realm-content ul ol,.realm-content ul ul{margin-top:.75rem;margin-bottom:.5rem;padding-left:1rem}.realm-content ul{list-style-type:disc}.realm-content ol{list-style-type:decimal}.realm-content table th:first-child,.realm-content td:first-child{padding-left:0}.realm-content table th:last-child,.realm-content td:last-child{padding-right:0}.realm-content abbr[title]{cursor:help;border-bottom-width:1px;border-style:dotted}.realm-content details{margin-top:1.25rem;margin-bottom:1.25rem}.realm-content summary{cursor:pointer;font-weight:700}.realm-content a code{color:inherit}.realm-content video{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-content math{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content small{font-size:.875rem}.realm-content del{text-decoration-line:line-through}.realm-content sub{vertical-align:sub;font-size:.75rem}.realm-content sup{vertical-align:super;font-size:.75rem}.realm-content button,.realm-content input{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}main :is(h1,h2,h3,h4){scroll-margin-top:6rem}::-moz-selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}::selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.sidemenu .peer:checked+label>svg{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.toc-expend-btn:has(#toc-expend:checked)+nav{display:block}.toc-expend-btn:has(#toc-expend:checked) .toc-expend-btn_ico{--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.main-header:has(#sidemenu-docs:checked)+main #sidebar #sidebar-docs,.main-header:has(#sidemenu-meta:checked)+main #sidebar #sidebar-meta,.main-header:has(#sidemenu-source:checked)+main #sidebar #sidebar-source,.main-header:has(#sidemenu-summary:checked)+main #sidebar #sidebar-summary{display:block}@media (min-width:40rem){:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .main-navigation,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main .realm-content{grid-column:span 6/span 6}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .sidemenu,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar{grid-column:span 4/span 4}}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar:before{position:absolute;top:0;left:-1.75rem;z-index:-1;display:block;height:100%;width:50vw;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}main :is(.source-code)>pre{overflow:scroll;border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;padding:1rem .25rem;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-size:.875rem}@media (min-width:40rem){main :is(.source-code)>pre{padding:2rem .75rem;font-size:1rem}}main .realm-content>pre a:hover{text-decoration-line:none}main :is(.realm-content,.source-code)>pre .chroma-ln:target{background-color:transparent!important}main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target) .chroma-cl,main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl{border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(226 226 226/var(--tw-bg-opacity))!important}main :is(.realm-content,.source-code)>pre .chroma-ln{scroll-margin-top:6rem}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.bottom-1{bottom:.25rem}.left-0{left:0}.right-2{right:.5rem}.right-3{right:.75rem}.top-0{top:0}.top-1\/2{top:50%}.top-14{top:3.5rem}.top-2{top:.5rem}.z-1{z-index:1}.z-max{z-index:9999}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-3{grid-column:span 3/span 3}.col-span-7{grid-column:span 7/span 7}.row-span-1{grid-row:span 1/span 1}.row-start-1{grid-row-start:1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.mr-10{margin-right:2.5rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.max-h-screen{max-height:100vh}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-full{width:100%}.min-w-2{min-width:.5rem}.min-w-48{min-width:12rem}.max-w-screen-max{max-width:98.75rem}.shrink-0{flex-shrink:0}.grow-\[2\]{flex-grow:2}.-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-flow-dense{grid-auto-flow:dense}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.375rem}.rounded-sm{border-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(153 153 153/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.bg-light{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.font-mono{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.text-100{font-size:.875rem}.text-200{font-size:1rem}.text-50{font-size:.75rem}.text-600{font-size:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-tight{line-height:1.25}.text-gray-300{--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(124 124 124/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(19 19 19/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.text-light{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.outline-none{outline:2px solid transparent;outline-offset:2px}.text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.\*\:pl-0>*{padding-left:0}.before\:px-\[0\.18rem\]:before{content:var(--tw-content);padding-left:.18rem;padding-right:.18rem}.before\:text-gray-300:before{content:var(--tw-content);--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.before\:content-\[\'\/\'\]:before{--tw-content:"/";content:var(--tw-content)}.before\:content-\[\'\:\'\]:before{--tw-content:":";content:var(--tw-content)}.before\:content-\[\'open\'\]:before{--tw-content:"open";content:var(--tw-content)}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-0:after{content:var(--tw-content);left:0}.after\:top-0:after{content:var(--tw-content);top:0}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:h-full:after{content:var(--tw-content);height:100%}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:rounded-t-sm:after{content:var(--tw-content);border-top-left-radius:.25rem;border-top-right-radius:.25rem}.after\:bg-gray-100:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.after\:bg-green-600:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.first\:border-t:first-child{border-top-width:1px}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.hover\:text-green-600:hover{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.hover\:text-light:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.focus\:border-l-gray-300:focus{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-gray-300{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-l-gray-300{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group.is-active .group-\[\.is-active\]\:text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.peer:checked~.peer-checked\:before\:content-\[\'close\'\]:before{--tw-content:"close";content:var(--tw-content)}.peer:focus-within~.peer-focus-within\:hidden{display:none}.has-\[ul\:empty\]\:hidden:has(ul:empty){display:none}.has-\[\:focus-within\]\:border-gray-300:has(:focus-within){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.has-\[\:focus\]\:border-gray-300:has(:focus){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}@media (min-width:30rem){.sm\:gap-6{gap:1.5rem}}@media (min-width:40rem){.md\:col-span-3{grid-column:span 3/span 3}.md\:mb-0{margin-bottom:0}.md\:h-4{height:1rem}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:pb-0{padding-bottom:0}}@media (min-width:51.25rem){.lg\:order-2{order:2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-start-1{grid-row-start:1}.lg\:row-start-2{grid-row-start:2}.lg\:mb-4{margin-bottom:1rem}.lg\:mt-0{margin-top:0}.lg\:mt-10{margin-top:2.5rem}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:justify-start{justify-content:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.lg\:border-none{border-style:none}.lg\:bg-transparent{background-color:transparent}.lg\:p-0{padding:0}.lg\:px-0{padding-left:0;padding-right:0}.lg\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.lg\:pb-28{padding-bottom:7rem}.lg\:pt-2{padding-top:.5rem}.lg\:text-200{font-size:1rem}.lg\:font-semibold{font-weight:600}.lg\:hover\:bg-transparent:hover{background-color:transparent}}@media (min-width:63.75rem){.xl\:inline{display:inline}.xl\:hidden{display:none}.xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:gap-20{gap:5rem}.xl\:gap-6{gap:1.5rem}.xl\:pt-0{padding-top:0}}@media (min-width:85.375rem){.xxl\:inline-block{display:inline-block}.xxl\:h-4{height:1rem}.xxl\:w-4{width:1rem}.xxl\:gap-20{gap:5rem}.xxl\:gap-x-32{-moz-column-gap:8rem;column-gap:8rem}.xxl\:pr-1{padding-right:.25rem}}
\ No newline at end of file
diff --git a/gno.land/pkg/gnoweb/url.go b/gno.land/pkg/gnoweb/url.go
index bc03f2182d9..9127225d490 100644
--- a/gno.land/pkg/gnoweb/url.go
+++ b/gno.land/pkg/gnoweb/url.go
@@ -4,145 +4,253 @@ import (
"errors"
"fmt"
"net/url"
+ "path/filepath"
"regexp"
+ "slices"
"strings"
)
-type PathKind byte
+var ErrURLInvalidPath = errors.New("invalid path")
-const (
- KindInvalid PathKind = 0
- KindRealm PathKind = 'r'
- KindPure PathKind = 'p'
-)
+// rePkgOrRealmPath matches and validates a flexible path.
+var rePkgOrRealmPath = regexp.MustCompile(`^/[a-z][a-z0-9_/]*$`)
// GnoURL decomposes the parts of an URL to query a realm.
type GnoURL struct {
// Example full path:
- // gno.land/r/demo/users:jae$help&a=b?c=d
+ // gno.land/r/demo/users/render.gno:jae$help&a=b?c=d
Domain string // gno.land
Path string // /r/demo/users
Args string // jae
WebQuery url.Values // help&a=b
Query url.Values // c=d
+ File string // render.gno
}
-func (url GnoURL) EncodeArgs() string {
+// EncodeFlag is used to specify which URL components to encode.
+type EncodeFlag int
+
+const (
+ EncodeDomain EncodeFlag = 1 << iota // Encode the domain component
+ EncodePath // Encode the path component
+ EncodeArgs // Encode the arguments component
+ EncodeWebQuery // Encode the web query component
+ EncodeQuery // Encode the query component
+ EncodeNoEscape // Disable escaping of arguments
+)
+
+// Encode constructs a URL string from the components of a GnoURL struct,
+// encoding the specified components based on the provided EncodeFlag bitmask.
+//
+// The function selectively encodes the URL's path, arguments, web query, and
+// query parameters, depending on the flags set in encodeFlags.
+//
+// Returns a string representing the encoded URL.
+//
+// Example:
+//
+// gnoURL := GnoURL{
+// Domain: "gno.land",
+// Path: "/r/demo/users",
+// Args: "john",
+// File: "render.gno",
+// }
+//
+// encodedURL := gnoURL.Encode(EncodePath | EncodeArgs)
+// fmt.Println(encodedURL) // Output: /r/demo/users/render.gno:john
+//
+// URL components are encoded using url.PathEscape unless EncodeNoEscape is specified.
+func (gnoURL GnoURL) Encode(encodeFlags EncodeFlag) string {
var urlstr strings.Builder
- if url.Args != "" {
- urlstr.WriteString(url.Args)
- }
- if len(url.Query) > 0 {
- urlstr.WriteString("?" + url.Query.Encode())
- }
+ noEscape := encodeFlags.Has(EncodeNoEscape)
- return urlstr.String()
-}
+ if encodeFlags.Has(EncodeDomain) {
+ urlstr.WriteString(gnoURL.Domain)
+ }
-func (url GnoURL) EncodePath() string {
- var urlstr strings.Builder
- urlstr.WriteString(url.Path)
- if url.Args != "" {
- urlstr.WriteString(":" + url.Args)
+ if encodeFlags.Has(EncodePath) {
+ path := gnoURL.Path
+ urlstr.WriteString(path)
}
- if len(url.Query) > 0 {
- urlstr.WriteString("?" + url.Query.Encode())
+ if len(gnoURL.File) > 0 {
+ urlstr.WriteRune('/')
+ urlstr.WriteString(gnoURL.File)
}
- return urlstr.String()
-}
+ if encodeFlags.Has(EncodeArgs) && gnoURL.Args != "" {
+ if encodeFlags.Has(EncodePath) {
+ urlstr.WriteRune(':')
+ }
-func (url GnoURL) EncodeWebPath() string {
- var urlstr strings.Builder
- urlstr.WriteString(url.Path)
- if url.Args != "" {
- pathEscape := escapeDollarSign(url.Args)
- urlstr.WriteString(":" + pathEscape)
+ // XXX: Arguments should ideally always be escaped,
+ // but this may require changes in some realms.
+ args := gnoURL.Args
+ if !noEscape {
+ args = escapeDollarSign(url.PathEscape(args))
+ }
+
+ urlstr.WriteString(args)
}
- if len(url.WebQuery) > 0 {
- urlstr.WriteString("$" + url.WebQuery.Encode())
+ if encodeFlags.Has(EncodeWebQuery) && len(gnoURL.WebQuery) > 0 {
+ urlstr.WriteRune('$')
+ if noEscape {
+ urlstr.WriteString(NoEscapeQuery(gnoURL.WebQuery))
+ } else {
+ urlstr.WriteString(gnoURL.WebQuery.Encode())
+ }
}
- if len(url.Query) > 0 {
- urlstr.WriteString("?" + url.Query.Encode())
+ if encodeFlags.Has(EncodeQuery) && len(gnoURL.Query) > 0 {
+ urlstr.WriteRune('?')
+ if noEscape {
+ urlstr.WriteString(NoEscapeQuery(gnoURL.Query))
+ } else {
+ urlstr.WriteString(gnoURL.Query.Encode())
+ }
}
return urlstr.String()
}
-func (url GnoURL) Kind() PathKind {
- if len(url.Path) < 2 {
- return KindInvalid
- }
- pk := PathKind(url.Path[1])
- switch pk {
- case KindPure, KindRealm:
- return pk
- }
- return KindInvalid
+// Has checks if the EncodeFlag contains all the specified flags.
+func (f EncodeFlag) Has(flags EncodeFlag) bool {
+ return f&flags != 0
}
-var (
- ErrURLMalformedPath = errors.New("malformed URL path")
- ErrURLInvalidPathKind = errors.New("invalid path kind")
-)
+func escapeDollarSign(s string) string {
+ return strings.ReplaceAll(s, "$", "%24")
+}
-// reRealName match a realm path
-// - matches[1]: path
-// - matches[2]: path args
-var reRealmPath = regexp.MustCompile(`^` +
- `(/(?:[a-zA-Z0-9_-]+)/` + // path kind
- `[a-zA-Z][a-zA-Z0-9_-]*` + // First path segment
- `(?:/[a-zA-Z][.a-zA-Z0-9_-]*)*/?)` + // Additional path segments
- `([:$](?:.*))?$`, // Remaining portions args, separate by `$` or `:`
-)
+// EncodeArgs encodes the arguments and query parameters into a string.
+// This function is intended to be passed as a realm `Render` argument.
+func (gnoURL GnoURL) EncodeArgs() string {
+ return gnoURL.Encode(EncodeArgs | EncodeQuery | EncodeNoEscape)
+}
+// EncodeURL encodes the path, arguments, and query parameters into a string.
+// This function provides the full representation of the URL without the web query.
+func (gnoURL GnoURL) EncodeURL() string {
+ return gnoURL.Encode(EncodePath | EncodeArgs | EncodeQuery)
+}
+
+// EncodeWebURL encodes the path, package arguments, web query, and query into a string.
+// This function provides the full representation of the URL.
+func (gnoURL GnoURL) EncodeWebURL() string {
+ return gnoURL.Encode(EncodePath | EncodeArgs | EncodeWebQuery | EncodeQuery)
+}
+
+// IsPure checks if the URL path represents a pure path.
+func (gnoURL GnoURL) IsPure() bool {
+ return strings.HasPrefix(gnoURL.Path, "/p/")
+}
+
+// IsRealm checks if the URL path represents a realm path.
+func (gnoURL GnoURL) IsRealm() bool {
+ return strings.HasPrefix(gnoURL.Path, "/r/")
+}
+
+// IsFile checks if the URL path represents a file.
+func (gnoURL GnoURL) IsFile() bool {
+ return gnoURL.File != ""
+}
+
+// IsDir checks if the URL path represents a directory.
+func (gnoURL GnoURL) IsDir() bool {
+ return !gnoURL.IsFile() &&
+ len(gnoURL.Path) > 0 && gnoURL.Path[len(gnoURL.Path)-1] == '/'
+}
+
+func (gnoURL GnoURL) IsValid() bool {
+ return rePkgOrRealmPath.MatchString(gnoURL.Path)
+}
+
+// ParseGnoURL parses a URL into a GnoURL structure, extracting and validating its components.
func ParseGnoURL(u *url.URL) (*GnoURL, error) {
- matches := reRealmPath.FindStringSubmatch(u.EscapedPath())
- if len(matches) != 3 {
- return nil, fmt.Errorf("%w: %s", ErrURLMalformedPath, u.Path)
+ var webargs string
+ path, args, found := strings.Cut(u.EscapedPath(), ":")
+ if found {
+ args, webargs, _ = strings.Cut(args, "$")
+ } else {
+ path, webargs, _ = strings.Cut(path, "$")
+ }
+
+ upath, err := url.PathUnescape(path)
+ if err != nil {
+ return nil, fmt.Errorf("unable to unescape path %q: %w", path, err)
}
- path := matches[1]
- args := matches[2]
+ var file string
- if len(args) > 0 {
- switch args[0] {
- case ':':
- args = args[1:]
- case '$':
- default:
- return nil, fmt.Errorf("%w: %s", ErrURLMalformedPath, u.Path)
+ // A file is considered as one that either ends with an extension or
+ // contains an uppercase rune
+ ext := filepath.Ext(upath)
+ base := filepath.Base(upath)
+ if ext != "" || strings.ToLower(base) != base {
+ file = base
+ upath = strings.TrimSuffix(upath, base)
+
+ // Trim last slash if any
+ if i := strings.LastIndexByte(upath, '/'); i > 0 {
+ upath = upath[:i]
}
}
- var err error
+ if !rePkgOrRealmPath.MatchString(upath) {
+ return nil, fmt.Errorf("%w: %q", ErrURLInvalidPath, upath)
+ }
+
webquery := url.Values{}
- args, webargs, found := strings.Cut(args, "$")
- if found {
- if webquery, err = url.ParseQuery(webargs); err != nil {
- return nil, fmt.Errorf("unable to parse webquery %q: %w ", webquery, err)
+ if len(webargs) > 0 {
+ var parseErr error
+ if webquery, parseErr = url.ParseQuery(webargs); parseErr != nil {
+ return nil, fmt.Errorf("unable to parse webquery %q: %w", webargs, parseErr)
}
}
uargs, err := url.PathUnescape(args)
if err != nil {
- return nil, fmt.Errorf("unable to unescape path %q: %w", args, err)
+ return nil, fmt.Errorf("unable to unescape args %q: %w", args, err)
}
return &GnoURL{
- Path: path,
+ Path: upath,
Args: uargs,
WebQuery: webquery,
Query: u.Query(),
Domain: u.Hostname(),
+ File: file,
}, nil
}
-func escapeDollarSign(s string) string {
- return strings.ReplaceAll(s, "$", "%24")
+// NoEscapeQuery generates a URL-encoded query string from the given url.Values,
+// without escaping the keys and values. The query parameters are sorted by key.
+func NoEscapeQuery(v url.Values) string {
+ // Encode encodes the values into βURL encodedβ form
+ // ("bar=baz&foo=quux") sorted by key.
+ if len(v) == 0 {
+ return ""
+ }
+ var buf strings.Builder
+ keys := make([]string, 0, len(v))
+ for k := range v {
+ keys = append(keys, k)
+ }
+ slices.Sort(keys)
+ for _, k := range keys {
+ vs := v[k]
+ keyEscaped := k
+ for _, v := range vs {
+ if buf.Len() > 0 {
+ buf.WriteByte('&')
+ }
+ buf.WriteString(keyEscaped)
+ buf.WriteByte('=')
+ buf.WriteString(v)
+ }
+ }
+ return buf.String()
}
diff --git a/gno.land/pkg/gnoweb/url_test.go b/gno.land/pkg/gnoweb/url_test.go
index 73cfdda69bd..7a491eaa149 100644
--- a/gno.land/pkg/gnoweb/url_test.go
+++ b/gno.land/pkg/gnoweb/url_test.go
@@ -19,8 +19,9 @@ func TestParseGnoURL(t *testing.T) {
Name: "malformed url",
Input: "https://gno.land/r/dem)o:$?",
Expected: nil,
- Err: ErrURLMalformedPath,
+ Err: ErrURLInvalidPath,
},
+
{
Name: "simple",
Input: "https://gno.land/r/simple/test",
@@ -30,8 +31,32 @@ func TestParseGnoURL(t *testing.T) {
WebQuery: url.Values{},
Query: url.Values{},
},
- Err: nil,
},
+
+ {
+ Name: "file",
+ Input: "https://gno.land/r/simple/test/encode.gno",
+ Expected: &GnoURL{
+ Domain: "gno.land",
+ Path: "/r/simple/test",
+ WebQuery: url.Values{},
+ Query: url.Values{},
+ File: "encode.gno",
+ },
+ },
+
+ {
+ Name: "complex file path",
+ Input: "https://gno.land/r/simple/test///...gno",
+ Expected: &GnoURL{
+ Domain: "gno.land",
+ Path: "/r/simple/test//",
+ WebQuery: url.Values{},
+ Query: url.Values{},
+ File: "...gno",
+ },
+ },
+
{
Name: "webquery + query",
Input: "https://gno.land/r/demo/foo$help&func=Bar&name=Baz",
@@ -46,7 +71,6 @@ func TestParseGnoURL(t *testing.T) {
Query: url.Values{},
Domain: "gno.land",
},
- Err: nil,
},
{
@@ -61,7 +85,6 @@ func TestParseGnoURL(t *testing.T) {
Query: url.Values{},
Domain: "gno.land",
},
- Err: nil,
},
{
@@ -78,7 +101,6 @@ func TestParseGnoURL(t *testing.T) {
},
Domain: "gno.land",
},
- Err: nil,
},
{
@@ -93,7 +115,6 @@ func TestParseGnoURL(t *testing.T) {
},
Domain: "gno.land",
},
- Err: nil,
},
{
@@ -108,22 +129,140 @@ func TestParseGnoURL(t *testing.T) {
Query: url.Values{},
Domain: "gno.land",
},
- Err: nil,
},
- // XXX: more tests
+ {
+ Name: "unknown path kind",
+ Input: "https://gno.land/x/demo/foo",
+ Expected: &GnoURL{
+ Path: "/x/demo/foo",
+ Args: "",
+ WebQuery: url.Values{},
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "empty path",
+ Input: "https://gno.land/r/",
+ Expected: &GnoURL{
+ Path: "/r/",
+ Args: "",
+ WebQuery: url.Values{},
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "complex query",
+ Input: "https://gno.land/r/demo/foo$help?func=Bar&name=Baz&age=30",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ Args: "",
+ WebQuery: url.Values{
+ "help": []string{""},
+ },
+ Query: url.Values{
+ "func": []string{"Bar"},
+ "name": []string{"Baz"},
+ "age": []string{"30"},
+ },
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "multiple web queries",
+ Input: "https://gno.land/r/demo/foo$help&func=Bar$test=123",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ Args: "",
+ WebQuery: url.Values{
+ "help": []string{""},
+ "func": []string{"Bar$test=123"},
+ },
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "webquery-args-webquery",
+ Input: "https://gno.land/r/demo/aaa$bbb:CCC&DDD$EEE",
+ Err: ErrURLInvalidPath, // `/r/demo/aaa$bbb` is an invalid path
+ },
+
+ {
+ Name: "args-webquery-args",
+ Input: "https://gno.land/r/demo/aaa:BBB$CCC&DDD:EEE",
+ Expected: &GnoURL{
+ Domain: "gno.land",
+ Path: "/r/demo/aaa",
+ Args: "BBB",
+ WebQuery: url.Values{
+ "CCC": []string{""},
+ "DDD:EEE": []string{""},
+ },
+ Query: url.Values{},
+ },
+ },
+
+ {
+ Name: "escaped characters in args",
+ Input: "https://gno.land/r/demo/foo:example%20with%20spaces$tz=Europe/Paris",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example with spaces",
+ WebQuery: url.Values{
+ "tz": []string{"Europe/Paris"},
+ },
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "file in path + args + query",
+ Input: "https://gno.land/r/demo/foo/render.gno:example$tz=Europe/Paris",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ File: "render.gno",
+ Args: "example",
+ WebQuery: url.Values{
+ "tz": []string{"Europe/Paris"},
+ },
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "no extension file",
+ Input: "https://gno.land/r/demo/lIcEnSe",
+ Expected: &GnoURL{
+ Path: "/r/demo",
+ File: "lIcEnSe",
+ Args: "",
+ WebQuery: url.Values{},
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
+ t.Logf("testing input: %q", tc.Input)
+
u, err := url.Parse(tc.Input)
require.NoError(t, err)
result, err := ParseGnoURL(u)
if tc.Err == nil {
require.NoError(t, err)
- t.Logf("parsed: %s", result.EncodePath())
- t.Logf("parsed web: %s", result.EncodeWebPath())
+ t.Logf("encoded web path: %q", result.EncodeWebURL())
} else {
require.Error(t, err)
require.ErrorIs(t, err, tc.Err)
@@ -133,3 +272,191 @@ func TestParseGnoURL(t *testing.T) {
})
}
}
+
+func TestEncode(t *testing.T) {
+ testCases := []struct {
+ Name string
+ GnoURL GnoURL
+ EncodeFlags EncodeFlag
+ Expected string
+ }{
+ {
+ Name: "encode domain",
+ GnoURL: GnoURL{
+ Domain: "gno.land",
+ Path: "/r/demo/foo",
+ },
+ EncodeFlags: EncodeDomain,
+ Expected: "gno.land",
+ },
+
+ {
+ Name: "encode web query without escape",
+ GnoURL: GnoURL{
+ Domain: "gno.land",
+ Path: "/r/demo/foo",
+ WebQuery: url.Values{
+ "help": []string{""},
+ "fun$c": []string{"B$ ar"},
+ },
+ },
+ EncodeFlags: EncodeWebQuery | EncodeNoEscape,
+ Expected: "$fun$c=B$ ar&help=",
+ },
+
+ {
+ Name: "encode domain and path",
+ GnoURL: GnoURL{
+ Domain: "gno.land",
+ Path: "/r/demo/foo",
+ },
+ EncodeFlags: EncodeDomain | EncodePath,
+ Expected: "gno.land/r/demo/foo",
+ },
+
+ {
+ Name: "Encode Path Only",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ },
+ EncodeFlags: EncodePath,
+ Expected: "/r/demo/foo",
+ },
+
+ {
+ Name: "Encode Path and File",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ File: "render.gno",
+ },
+ EncodeFlags: EncodePath,
+ Expected: "/r/demo/foo/render.gno",
+ },
+
+ {
+ Name: "Encode Path, File, and Args",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ File: "render.gno",
+ Args: "example",
+ },
+ EncodeFlags: EncodePath | EncodeArgs,
+ Expected: "/r/demo/foo/render.gno:example",
+ },
+
+ {
+ Name: "Encode Path and Args",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ },
+ EncodeFlags: EncodePath | EncodeArgs,
+ Expected: "/r/demo/foo:example",
+ },
+
+ {
+ Name: "Encode Path, Args, and WebQuery",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ WebQuery: url.Values{
+ "tz": []string{"Europe/Paris"},
+ },
+ },
+ EncodeFlags: EncodePath | EncodeArgs | EncodeWebQuery,
+ Expected: "/r/demo/foo:example$tz=Europe%2FParis",
+ },
+
+ {
+ Name: "Encode Full URL",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ WebQuery: url.Values{
+ "tz": []string{"Europe/Paris"},
+ },
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ },
+ EncodeFlags: EncodePath | EncodeArgs | EncodeWebQuery | EncodeQuery,
+ Expected: "/r/demo/foo:example$tz=Europe%2FParis?hello=42",
+ },
+
+ {
+ Name: "Encode Args and Query",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "hello Jo$ny",
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ },
+ EncodeFlags: EncodeArgs | EncodeQuery,
+ Expected: "hello%20Jo%24ny?hello=42",
+ },
+
+ {
+ Name: "Encode Args and Query (No Escape)",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "hello Jo$ny",
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ },
+ EncodeFlags: EncodeArgs | EncodeQuery | EncodeNoEscape,
+ Expected: "hello Jo$ny?hello=42",
+ },
+
+ {
+ Name: "Encode Args and Query",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ },
+ EncodeFlags: EncodeArgs | EncodeQuery,
+ Expected: "example?hello=42",
+ },
+
+ {
+ Name: "Encode with Escaped Characters",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example with spaces",
+ WebQuery: url.Values{
+ "tz": []string{"Europe/Paris"},
+ },
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ },
+ EncodeFlags: EncodePath | EncodeArgs | EncodeWebQuery | EncodeQuery,
+ Expected: "/r/demo/foo:example%20with%20spaces$tz=Europe%2FParis?hello=42",
+ },
+
+ {
+ Name: "Encode Path, Args, and Query",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ },
+ EncodeFlags: EncodePath | EncodeArgs | EncodeQuery,
+ Expected: "/r/demo/foo:example?hello=42",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.Name, func(t *testing.T) {
+ result := tc.GnoURL.Encode(tc.EncodeFlags)
+ require.True(t, tc.GnoURL.IsValid(), "gno url is not valid")
+ assert.Equal(t, tc.Expected, result)
+ })
+ }
+}
diff --git a/gno.land/pkg/integration/node_testing.go b/gno.land/pkg/integration/node_testing.go
index fdf94c8c545..7965f228fc2 100644
--- a/gno.land/pkg/integration/node_testing.go
+++ b/gno.land/pkg/integration/node_testing.go
@@ -56,6 +56,7 @@ func TestingInMemoryNode(t TestingTS, logger *slog.Logger, config *gnoland.InMem
// It will return the default creator address of the loaded packages.
func TestingNodeConfig(t TestingTS, gnoroot string, additionalTxs ...gnoland.TxWithMetadata) (*gnoland.InMemoryNodeConfig, bft.Address) {
cfg := TestingMinimalNodeConfig(gnoroot)
+ cfg.SkipGenesisVerification = true
creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1
diff --git a/gno.land/pkg/integration/pkgloader.go b/gno.land/pkg/integration/pkgloader.go
index 168242df97e..e40e8ff1eb5 100644
--- a/gno.land/pkg/integration/pkgloader.go
+++ b/gno.land/pkg/integration/pkgloader.go
@@ -10,7 +10,7 @@ import (
"github.com/gnolang/gno/gnovm/pkg/gnolang"
"github.com/gnolang/gno/gnovm/pkg/gnomod"
"github.com/gnolang/gno/gnovm/pkg/packages"
- bft "github.com/gnolang/gno/tm2/pkg/bft/types"
+ "github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/std"
)
@@ -39,7 +39,7 @@ func (pl *PkgsLoader) SetPatch(replace, with string) {
pl.patchs[replace] = with
}
-func (pl *PkgsLoader) LoadPackages(creator bft.Address, fee std.Fee, deposit std.Coins) ([]gnoland.TxWithMetadata, error) {
+func (pl *PkgsLoader) LoadPackages(creatorKey crypto.PrivKey, fee std.Fee, deposit std.Coins) ([]gnoland.TxWithMetadata, error) {
pkgslist, err := pl.List().Sort() // sorts packages by their dependencies.
if err != nil {
return nil, fmt.Errorf("unable to sort packages: %w", err)
@@ -47,7 +47,7 @@ func (pl *PkgsLoader) LoadPackages(creator bft.Address, fee std.Fee, deposit std
txs := make([]gnoland.TxWithMetadata, len(pkgslist))
for i, pkg := range pkgslist {
- tx, err := gnoland.LoadPackage(pkg, creator, fee, deposit)
+ tx, err := gnoland.LoadPackage(pkg, creatorKey.PubKey().Address(), fee, deposit)
if err != nil {
return nil, fmt.Errorf("unable to load pkg %q: %w", pkg.Name, err)
}
@@ -77,6 +77,11 @@ func (pl *PkgsLoader) LoadPackages(creator bft.Address, fee std.Fee, deposit std
}
}
+ err = SignTxs(txs, creatorKey, "tendermint_test")
+ if err != nil {
+ return nil, fmt.Errorf("unable to sign txs: %w", err)
+ }
+
return txs, nil
}
diff --git a/gno.land/pkg/integration/signer.go b/gno.land/pkg/integration/signer.go
new file mode 100644
index 00000000000..b32cd9c59bc
--- /dev/null
+++ b/gno.land/pkg/integration/signer.go
@@ -0,0 +1,33 @@
+package integration
+
+import (
+ "fmt"
+
+ "github.com/gnolang/gno/gno.land/pkg/gnoland"
+
+ "github.com/gnolang/gno/tm2/pkg/crypto"
+ "github.com/gnolang/gno/tm2/pkg/std"
+)
+
+// SignTxs will sign all txs passed as argument using the private key
+// this signature is only valid for genesis transactions as accountNumber and sequence are 0
+func SignTxs(txs []gnoland.TxWithMetadata, privKey crypto.PrivKey, chainID string) error {
+ for index, tx := range txs {
+ bytes, err := tx.Tx.GetSignBytes(chainID, 0, 0)
+ if err != nil {
+ return fmt.Errorf("unable to get sign bytes for transaction, %w", err)
+ }
+ signature, err := privKey.Sign(bytes)
+ if err != nil {
+ return fmt.Errorf("unable to sign transaction, %w", err)
+ }
+
+ txs[index].Tx.Signatures = []std.Signature{
+ {
+ PubKey: privKey.PubKey(),
+ Signature: signature,
+ },
+ }
+ }
+ return nil
+}
diff --git a/gno.land/pkg/integration/testdata/event_multi_msg.txtar b/gno.land/pkg/integration/testdata/event_multi_msg.txtar
index 84afe3cc6a4..13a448e7f8c 100644
--- a/gno.land/pkg/integration/testdata/event_multi_msg.txtar
+++ b/gno.land/pkg/integration/testdata/event_multi_msg.txtar
@@ -11,16 +11,19 @@ stdout 'data: {'
stdout ' "BaseAccount": {'
stdout ' "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",'
stdout ' "coins": "[0-9]*ugnot",' # dynamic
-stdout ' "public_key": null,'
+stdout ' "public_key": {'
+stdout ' "@type": "/tm.PubKeySecp256k1",'
+stdout ' "value": "A\+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"'
+stdout ' },'
stdout ' "account_number": "0",'
-stdout ' "sequence": "0"'
+stdout ' "sequence": "1"'
stdout ' }'
stdout '}'
! stderr '.+' # empty
## sign
-gnokey sign -tx-path $WORK/multi/multi_msg.tx -chainid=tendermint_test -account-number 0 -account-sequence 0 test1
+gnokey sign -tx-path $WORK/multi/multi_msg.tx -chainid=tendermint_test -account-number 0 -account-sequence 1 test1
stdout 'Tx successfully signed and saved to '
## broadcast
diff --git a/gno.land/pkg/integration/testdata/gnokey_simulate.txtar b/gno.land/pkg/integration/testdata/gnokey_simulate.txtar
index 8db2c7302fc..db3cd527eb3 100644
--- a/gno.land/pkg/integration/testdata/gnokey_simulate.txtar
+++ b/gno.land/pkg/integration/testdata/gnokey_simulate.txtar
@@ -7,41 +7,41 @@ gnoland start
# Initial state: assert that sequence == 0.
gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "0"'
+stdout '"sequence": "1"'
# attempt adding the "test" package.
# the package has a syntax error; simulation should catch this ahead of time and prevent the tx.
# -simulate test
! gnokey maketx addpkg -pkgdir $WORK/test -pkgpath gno.land/r/test -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate test test1
gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "0"'
+stdout '"sequence": "1"'
# -simulate only
! gnokey maketx addpkg -pkgdir $WORK/test -pkgpath gno.land/r/test -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1
gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "0"'
+stdout '"sequence": "1"'
# -simulate skip
! gnokey maketx addpkg -pkgdir $WORK/test -pkgpath gno.land/r/test -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1
gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "1"'
+stdout '"sequence": "2"'
# attempt calling hello.SetName correctly.
# -simulate test and skip should do it successfully, -simulate only should not.
# -simulate test
gnokey maketx call -pkgpath gno.land/r/hello -func SetName -args John -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate test test1
gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "2"'
+stdout '"sequence": "3"'
gnokey query vm/qeval --data "gno.land/r/hello.Hello()"
stdout 'Hello, John!'
# -simulate only
gnokey maketx call -pkgpath gno.land/r/hello -func SetName -args Paul -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate only test1
gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "2"'
+stdout '"sequence": "3"'
gnokey query vm/qeval --data "gno.land/r/hello.Hello()"
stdout 'Hello, John!'
# -simulate skip
gnokey maketx call -pkgpath gno.land/r/hello -func SetName -args George -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate skip test1
gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "3"'
+stdout '"sequence": "4"'
gnokey query vm/qeval --data "gno.land/r/hello.Hello()"
stdout 'Hello, George!'
@@ -51,19 +51,19 @@ stdout 'Hello, George!'
# -simulate test
! gnokey maketx call -pkgpath gno.land/r/hello -func Grumpy -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate test test1
gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "3"'
+stdout '"sequence": "4"'
gnokey query vm/qeval --data "gno.land/r/hello.Hello()"
stdout 'Hello, George!'
# -simulate only
! gnokey maketx call -pkgpath gno.land/r/hello -func Grumpy -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate only test1
gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "3"'
+stdout '"sequence": "4"'
gnokey query vm/qeval --data "gno.land/r/hello.Hello()"
stdout 'Hello, George!'
# -simulate skip
! gnokey maketx call -pkgpath gno.land/r/hello -func Grumpy -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate skip test1
gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "4"'
+stdout '"sequence": "5"'
gnokey query vm/qeval --data "gno.land/r/hello.Hello()"
stdout 'Hello, George!'
diff --git a/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar b/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar
index 3ed35a1b1d3..02bd8058214 100644
--- a/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar
+++ b/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar
@@ -14,9 +14,12 @@ stdout 'data: {'
stdout ' "BaseAccount": {'
stdout ' "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",'
stdout ' "coins": "[0-9]*ugnot",' # dynamic
-stdout ' "public_key": null,'
+stdout ' "public_key": {'
+stdout ' "@type": "/tm.PubKeySecp256k1",'
+stdout ' "value": "A\+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"'
+stdout ' },'
stdout ' "account_number": "0",'
-stdout ' "sequence": "0"'
+stdout ' "sequence": "4"'
stdout ' }'
stdout '}'
! stderr '.+' # empty
@@ -26,7 +29,7 @@ gnokey maketx call -pkgpath "gno.land/r/demo/echo" -func "Render" -gas-fee 10000
cp stdout call.tx
# Sign
-gnokey sign -tx-path $WORK/call.tx -chainid "tendermint_test" -account-number 0 -account-sequence 0 test1
+gnokey sign -tx-path $WORK/call.tx -chainid "tendermint_test" -account-number 0 -account-sequence 4 test1
cmpenv stdout sign.stdout.golden
gnokey broadcast $WORK/call.tx
diff --git a/gno.land/pkg/integration/testdata/restart_missing_type.txtar b/gno.land/pkg/integration/testdata/restart_missing_type.txtar
index b02acc16d96..09e1a27d6f4 100644
--- a/gno.land/pkg/integration/testdata/restart_missing_type.txtar
+++ b/gno.land/pkg/integration/testdata/restart_missing_type.txtar
@@ -5,15 +5,15 @@
loadpkg gno.land/p/demo/avl
gnoland start
-gnokey sign -tx-path $WORK/tx1.tx -chainid tendermint_test -account-sequence 0 test1
+gnokey sign -tx-path $WORK/tx1.tx -chainid tendermint_test -account-sequence 1 test1
! gnokey broadcast $WORK/tx1.tx
stderr 'out of gas'
-gnokey sign -tx-path $WORK/tx2.tx -chainid tendermint_test -account-sequence 1 test1
+gnokey sign -tx-path $WORK/tx2.tx -chainid tendermint_test -account-sequence 2 test1
gnokey broadcast $WORK/tx2.tx
stdout 'OK!'
-gnokey sign -tx-path $WORK/tx3.tx -chainid tendermint_test -account-sequence 2 test1
+gnokey sign -tx-path $WORK/tx3.tx -chainid tendermint_test -account-sequence 3 test1
gnokey broadcast $WORK/tx3.tx
stdout 'OK!'
diff --git a/gno.land/pkg/integration/testdata/simulate_gas.txtar b/gno.land/pkg/integration/testdata/simulate_gas.txtar
index 8550419f205..4c5213da345 100644
--- a/gno.land/pkg/integration/testdata/simulate_gas.txtar
+++ b/gno.land/pkg/integration/testdata/simulate_gas.txtar
@@ -6,11 +6,11 @@ gnoland start
# simulate only
gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1
-stdout 'GAS USED: 96411'
+stdout 'GAS USED: 99015'
# simulate skip
gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1
-stdout 'GAS USED: 96411' # same as simulate only
+stdout 'GAS USED: 99015' # same as simulate only
-- package/package.gno --
diff --git a/gno.land/pkg/integration/testscript_gnoland.go b/gno.land/pkg/integration/testscript_gnoland.go
index ae484a07669..9781799ea7d 100644
--- a/gno.land/pkg/integration/testscript_gnoland.go
+++ b/gno.land/pkg/integration/testscript_gnoland.go
@@ -117,7 +117,7 @@ func SetupGnolandTestscript(t *testing.T, p *testscript.Params) error {
nodesManager := NewNodesManager()
- defaultPK, err := generatePrivKeyFromMnemonic(DefaultAccount_Seed, "", 0, 0)
+ defaultPK, err := GeneratePrivKeyFromMnemonic(DefaultAccount_Seed, "", 0, 0)
require.NoError(t, err)
var buildOnce sync.Once
@@ -237,6 +237,9 @@ func SetupGnolandTestscript(t *testing.T, p *testscript.Params) error {
func gnolandCmd(t *testing.T, nodesManager *NodesManager, gnoRootDir string) func(ts *testscript.TestScript, neg bool, args []string) {
t.Helper()
+ defaultPK, err := GeneratePrivKeyFromMnemonic(DefaultAccount_Seed, "", 0, 0)
+ require.NoError(t, err)
+
return func(ts *testscript.TestScript, neg bool, args []string) {
sid := getNodeSID(ts)
@@ -265,9 +268,8 @@ func gnolandCmd(t *testing.T, nodesManager *NodesManager, gnoRootDir string) fun
}
pkgs := ts.Value(envKeyPkgsLoader).(*PkgsLoader)
- creator := crypto.MustAddressFromString(DefaultAccount_Address)
defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000)))
- pkgsTxs, err := pkgs.LoadPackages(creator, defaultFee, nil)
+ pkgsTxs, err := pkgs.LoadPackages(defaultPK, defaultFee, nil)
if err != nil {
ts.Fatalf("unable to load packages txs: %s", err)
}
@@ -765,7 +767,7 @@ func buildGnoland(t *testing.T, rootdir string) string {
}
// GeneratePrivKeyFromMnemonic generates a crypto.PrivKey from a mnemonic.
-func generatePrivKeyFromMnemonic(mnemonic, bip39Passphrase string, account, index uint32) (crypto.PrivKey, error) {
+func GeneratePrivKeyFromMnemonic(mnemonic, bip39Passphrase string, account, index uint32) (crypto.PrivKey, error) {
// Generate Seed from Mnemonic
seed, err := bip39.NewSeedWithErrorChecking(mnemonic, bip39Passphrase)
if err != nil {
diff --git a/gnovm/pkg/gnolang/bench_test.go b/gnovm/pkg/gnolang/bench_test.go
new file mode 100644
index 00000000000..b638ab66cd0
--- /dev/null
+++ b/gnovm/pkg/gnolang/bench_test.go
@@ -0,0 +1,31 @@
+package gnolang
+
+import (
+ "testing"
+)
+
+var sink any = nil
+
+var pkgIDPaths = []string{
+ "encoding/json",
+ "math/bits",
+ "github.com/gnolang/gno/gnovm/pkg/gnolang",
+ "a",
+ " ",
+ "",
+ "github.com/gnolang/gno/gnovm/pkg/gnolang/vendor/pkg/github.com/gnolang/vendored",
+}
+
+func BenchmarkPkgIDFromPkgPath(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ for _, path := range pkgIDPaths {
+ sink = PkgIDFromPkgPath(path)
+ }
+ }
+
+ if sink == nil {
+ b.Fatal("Benchmark did not run!")
+ }
+ sink = nil
+}
diff --git a/gnovm/pkg/gnolang/fuzz_test.go b/gnovm/pkg/gnolang/fuzz_test.go
new file mode 100644
index 00000000000..977c7453b90
--- /dev/null
+++ b/gnovm/pkg/gnolang/fuzz_test.go
@@ -0,0 +1,89 @@
+package gnolang
+
+import (
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/cockroachdb/apd/v3"
+)
+
+func FuzzConvertUntypedBigdecToFloat(f *testing.F) {
+ // 1. Firstly add seeds.
+ seeds := []string{
+ "-100000",
+ "100000",
+ "0",
+ }
+
+ check := new(apd.Decimal)
+ for _, seed := range seeds {
+ if check.UnmarshalText([]byte(seed)) == nil {
+ f.Add(seed)
+ }
+ }
+
+ f.Fuzz(func(t *testing.T, apdStr string) {
+ switch {
+ case strings.HasPrefix(apdStr, ".-"):
+ return
+ }
+
+ v := new(apd.Decimal)
+ if err := v.UnmarshalText([]byte(apdStr)); err != nil {
+ return
+ }
+ if _, err := v.Float64(); err != nil {
+ return
+ }
+
+ bd := BigdecValue{
+ V: v,
+ }
+ dst := new(TypedValue)
+ typ := Float64Type
+ ConvertUntypedBigdecTo(dst, bd, typ)
+ })
+}
+
+func FuzzParseFile(f *testing.F) {
+ // 1. Add the corpra.
+ parseFileDir := filepath.Join("testdata", "corpra", "parsefile")
+ paths, err := filepath.Glob(filepath.Join(parseFileDir, "*.go"))
+ if err != nil {
+ f.Fatal(err)
+ }
+
+ // Also load in files from gno/gnovm/tests/files
+ pc, curFile, _, _ := runtime.Caller(0)
+ curFileDir := filepath.Dir(curFile)
+ gnovmTestFilesDir, err := filepath.Abs(filepath.Join(curFileDir, "..", "..", "tests", "files"))
+ if err != nil {
+ _ = pc // To silence the arbitrary golangci linter.
+ f.Fatal(err)
+ }
+ globGnoTestFiles := filepath.Join(gnovmTestFilesDir, "*.gno")
+ gnoTestFiles, err := filepath.Glob(globGnoTestFiles)
+ if err != nil {
+ f.Fatal(err)
+ }
+ if len(gnoTestFiles) == 0 {
+ f.Fatalf("no files found from globbing %q", globGnoTestFiles)
+ }
+ paths = append(paths, gnoTestFiles...)
+
+ for _, path := range paths {
+ blob, err := os.ReadFile(path)
+ if err != nil {
+ f.Fatal(err)
+ }
+ f.Add(string(blob))
+ }
+
+ // 2. Now run the fuzzer.
+ f.Fuzz(func(t *testing.T, goFileContents string) {
+ _, _ = ParseFile("a.go", goFileContents)
+ })
+}
diff --git a/gnovm/pkg/gnolang/gno_test.go b/gnovm/pkg/gnolang/gno_test.go
index 89458667997..5a8c6faf315 100644
--- a/gnovm/pkg/gnolang/gno_test.go
+++ b/gnovm/pkg/gnolang/gno_test.go
@@ -36,6 +36,8 @@ func setupMachine(b *testing.B, numValues, numStmts, numExprs, numBlocks, numFra
func BenchmarkStringLargeData(b *testing.B) {
m := setupMachine(b, 10000, 5000, 5000, 2000, 3000, 1000)
+ b.ResetTimer()
+ b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = m.String()
diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go
index 4480a89d16f..75d12ac5402 100644
--- a/gnovm/pkg/gnolang/machine.go
+++ b/gnovm/pkg/gnolang/machine.go
@@ -2117,6 +2117,10 @@ func (m *Machine) Printf(format string, args ...interface{}) {
}
func (m *Machine) String() string {
+ if m == nil {
+ return "Machine:nil"
+ }
+
// Calculate some reasonable total length to avoid reallocation
// Assuming an average length of 32 characters per string
var (
@@ -2131,25 +2135,26 @@ func (m *Machine) String() string {
totalLength = vsLength + ssLength + xsLength + bsLength + obsLength + fsLength + exceptionsLength
)
- var builder strings.Builder
+ var sb strings.Builder
+ builder := &sb // Pointer for use in fmt.Fprintf.
builder.Grow(totalLength)
- builder.WriteString(fmt.Sprintf("Machine:\n PreprocessorMode: %v\n Op: %v\n Values: (len: %d)\n", m.PreprocessorMode, m.Ops[:m.NumOps], m.NumValues))
+ fmt.Fprintf(builder, "Machine:\n PreprocessorMode: %v\n Op: %v\n Values: (len: %d)\n", m.PreprocessorMode, m.Ops[:m.NumOps], m.NumValues)
for i := m.NumValues - 1; i >= 0; i-- {
- builder.WriteString(fmt.Sprintf(" #%d %v\n", i, m.Values[i]))
+ fmt.Fprintf(builder, " #%d %v\n", i, m.Values[i])
}
builder.WriteString(" Exprs:\n")
for i := len(m.Exprs) - 1; i >= 0; i-- {
- builder.WriteString(fmt.Sprintf(" #%d %v\n", i, m.Exprs[i]))
+ fmt.Fprintf(builder, " #%d %v\n", i, m.Exprs[i])
}
builder.WriteString(" Stmts:\n")
for i := len(m.Stmts) - 1; i >= 0; i-- {
- builder.WriteString(fmt.Sprintf(" #%d %v\n", i, m.Stmts[i]))
+ fmt.Fprintf(builder, " #%d %v\n", i, m.Stmts[i])
}
builder.WriteString(" Blocks:\n")
@@ -2166,17 +2171,17 @@ func (m *Machine) String() string {
if pv, ok := b.Source.(*PackageNode); ok {
// package blocks have too much, so just
// print the pkgpath.
- builder.WriteString(fmt.Sprintf(" %s(%d) %s\n", gens, gen, pv.PkgPath))
+ fmt.Fprintf(builder, " %s(%d) %s\n", gens, gen, pv.PkgPath)
} else {
bsi := b.StringIndented(" ")
- builder.WriteString(fmt.Sprintf(" %s(%d) %s\n", gens, gen, bsi))
+ fmt.Fprintf(builder, " %s(%d) %s\n", gens, gen, bsi)
if b.Source != nil {
sb := b.GetSource(m.Store).GetStaticBlock().GetBlock()
- builder.WriteString(fmt.Sprintf(" (s vals) %s(%d) %s\n", gens, gen, sb.StringIndented(" ")))
+ fmt.Fprintf(builder, " (s vals) %s(%d) %s\n", gens, gen, sb.StringIndented(" "))
sts := b.GetSource(m.Store).GetStaticBlock().Types
- builder.WriteString(fmt.Sprintf(" (s typs) %s(%d) %s\n", gens, gen, sts))
+ fmt.Fprintf(builder, " (s typs) %s(%d) %s\n", gens, gen, sts)
}
}
@@ -2187,7 +2192,7 @@ func (m *Machine) String() string {
case *Block:
b = bp
case RefValue:
- builder.WriteString(fmt.Sprintf(" (block ref %v)\n", bp.ObjectID))
+ fmt.Fprintf(builder, " (block ref %v)\n", bp.ObjectID)
b = nil
default:
panic("should not happen")
@@ -2206,12 +2211,12 @@ func (m *Machine) String() string {
if _, ok := b.Source.(*PackageNode); ok {
break // done, skip *PackageNode.
} else {
- builder.WriteString(fmt.Sprintf(" #%d %s\n", i,
- b.StringIndented(" ")))
+ fmt.Fprintf(builder, " #%d %s\n", i,
+ b.StringIndented(" "))
if b.Source != nil {
sb := b.GetSource(m.Store).GetStaticBlock().GetBlock()
- builder.WriteString(fmt.Sprintf(" (static) #%d %s\n", i,
- sb.StringIndented(" ")))
+ fmt.Fprintf(builder, " (static) #%d %s\n", i,
+ sb.StringIndented(" "))
}
}
}
@@ -2219,17 +2224,17 @@ func (m *Machine) String() string {
builder.WriteString(" Frames:\n")
for i := len(m.Frames) - 1; i >= 0; i-- {
- builder.WriteString(fmt.Sprintf(" #%d %s\n", i, m.Frames[i]))
+ fmt.Fprintf(builder, " #%d %s\n", i, m.Frames[i])
}
if m.Realm != nil {
- builder.WriteString(fmt.Sprintf(" Realm:\n %s\n", m.Realm.Path))
+ fmt.Fprintf(builder, " Realm:\n %s\n", m.Realm.Path)
}
builder.WriteString(" Exceptions:\n")
for _, ex := range m.Exceptions {
- builder.WriteString(fmt.Sprintf(" %s\n", ex.Sprint(m)))
+ fmt.Fprintf(builder, " %s\n", ex.Sprint(m))
}
return builder.String()
diff --git a/gnovm/pkg/gnolang/machine_test.go b/gnovm/pkg/gnolang/machine_test.go
index c3b2118f099..c2ab8ea12c5 100644
--- a/gnovm/pkg/gnolang/machine_test.go
+++ b/gnovm/pkg/gnolang/machine_test.go
@@ -9,6 +9,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/store/dbadapter"
"github.com/gnolang/gno/tm2/pkg/store/iavl"
stypes "github.com/gnolang/gno/tm2/pkg/store/types"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
)
@@ -56,3 +57,61 @@ func TestRunMemPackageWithOverrides_revertToOld(t *testing.T) {
assert.Equal(t, StringKind, v.T.Kind())
assert.Equal(t, StringValue("1"), v.V)
}
+
+func TestMachineString(t *testing.T) {
+ cases := []struct {
+ name string
+ in *Machine
+ want string
+ }{
+ {
+ "nil Machine",
+ nil,
+ "Machine:nil",
+ },
+ {
+ "created with defaults",
+ NewMachineWithOptions(MachineOptions{}),
+ "Machine:\n PreprocessorMode: false\n Op: []\n Values: (len: 0)\n Exprs:\n Stmts:\n Blocks:\n Blocks (other):\n Frames:\n Exceptions:\n",
+ },
+ {
+ "created with store and defaults",
+ func() *Machine {
+ db := memdb.NewMemDB()
+ baseStore := dbadapter.StoreConstructor(db, stypes.StoreOptions{})
+ iavlStore := iavl.StoreConstructor(db, stypes.StoreOptions{})
+ store := NewStore(nil, baseStore, iavlStore)
+ return NewMachine("std", store)
+ }(),
+ "Machine:\n PreprocessorMode: false\n Op: []\n Values: (len: 0)\n Exprs:\n Stmts:\n Blocks:\n Blocks (other):\n Frames:\n Exceptions:\n",
+ },
+ {
+ "filled in",
+ func() *Machine {
+ db := memdb.NewMemDB()
+ baseStore := dbadapter.StoreConstructor(db, stypes.StoreOptions{})
+ iavlStore := iavl.StoreConstructor(db, stypes.StoreOptions{})
+ store := NewStore(nil, baseStore, iavlStore)
+ m := NewMachine("std", store)
+ m.PushOp(OpHalt)
+ m.PushExpr(&BasicLitExpr{
+ Kind: INT,
+ Value: "100",
+ })
+ m.Blocks = make([]*Block, 1, 1)
+ m.PushStmts(S(Call(X("Redecl"), 11)))
+ return m
+ }(),
+ "Machine:\n PreprocessorMode: false\n Op: [OpHalt]\n Values: (len: 0)\n Exprs:\n #0 100\n Stmts:\n #0 Redecl(11)\n Blocks:\n Blocks (other):\n Frames:\n Exceptions:\n",
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ got := tt.in.String()
+ if diff := cmp.Diff(got, tt.want); diff != "" {
+ t.Fatalf("Mismatch: got - want +\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/gnovm/pkg/gnolang/op_bench_test.go b/gnovm/pkg/gnolang/op_bench_test.go
new file mode 100644
index 00000000000..5874f980285
--- /dev/null
+++ b/gnovm/pkg/gnolang/op_bench_test.go
@@ -0,0 +1,70 @@
+package gnolang
+
+import (
+ "testing"
+
+ "github.com/gnolang/gno/tm2/pkg/overflow"
+)
+
+func BenchmarkOpAdd(b *testing.B) {
+ m := NewMachine("bench", nil)
+ x := TypedValue{T: IntType}
+ x.SetInt(4)
+ y := TypedValue{T: IntType}
+ y.SetInt(3)
+
+ b.ResetTimer()
+
+ for range b.N {
+ m.PushOp(OpHalt)
+ m.PushExpr(&BinaryExpr{})
+ m.PushValue(x)
+ m.PushValue(y)
+ m.PushOp(OpAdd)
+ m.Run()
+ }
+}
+
+//go:noinline
+func AddNoOverflow(x, y int) int { return x + y }
+
+func BenchmarkAddNoOverflow(b *testing.B) {
+ x, y := 4, 3
+ c := 0
+ for range b.N {
+ c = AddNoOverflow(x, y)
+ }
+ if c != 7 {
+ b.Error("invalid result")
+ }
+}
+
+func BenchmarkAddOverflow(b *testing.B) {
+ x, y := 4, 3
+ c := 0
+ for range b.N {
+ c = overflow.Addp(x, y)
+ }
+ if c != 7 {
+ b.Error("invalid result")
+ }
+}
+
+func TestOpAdd1(t *testing.T) {
+ m := NewMachine("test", nil)
+ a := TypedValue{T: IntType}
+ a.SetInt(4)
+ b := TypedValue{T: IntType}
+ b.SetInt(3)
+ t.Log("a:", a, "b:", b)
+
+ start := m.NumValues
+ m.PushOp(OpHalt)
+ m.PushExpr(&BinaryExpr{})
+ m.PushValue(a)
+ m.PushValue(b)
+ m.PushOp(OpAdd)
+ m.Run()
+ res := m.ReapValues(start)
+ t.Log("res:", res)
+}
diff --git a/gnovm/pkg/gnolang/op_binary.go b/gnovm/pkg/gnolang/op_binary.go
index 6d26fa7ce54..0f66da5e685 100644
--- a/gnovm/pkg/gnolang/op_binary.go
+++ b/gnovm/pkg/gnolang/op_binary.go
@@ -6,6 +6,7 @@ import (
"math/big"
"github.com/cockroachdb/apd/v3"
+ "github.com/gnolang/gno/tm2/pkg/overflow"
)
// ----------------------------------------
@@ -183,7 +184,9 @@ func (m *Machine) doOpAdd() {
}
// add rv to lv.
- addAssign(m.Alloc, lv, rv)
+ if err := addAssign(m.Alloc, lv, rv); err != nil {
+ panic(err)
+ }
}
func (m *Machine) doOpSub() {
@@ -197,7 +200,9 @@ func (m *Machine) doOpSub() {
}
// sub rv from lv.
- subAssign(lv, rv)
+ if err := subAssign(lv, rv); err != nil {
+ panic(err)
+ }
}
func (m *Machine) doOpBor() {
@@ -253,8 +258,7 @@ func (m *Machine) doOpQuo() {
}
// lv / rv
- err := quoAssign(lv, rv)
- if err != nil {
+ if err := quoAssign(lv, rv); err != nil {
panic(err)
}
}
@@ -270,8 +274,7 @@ func (m *Machine) doOpRem() {
}
// lv % rv
- err := remAssign(lv, rv)
- if err != nil {
+ if err := remAssign(lv, rv); err != nil {
panic(err)
}
}
@@ -683,23 +686,38 @@ func isGeq(lv, rv *TypedValue) bool {
}
}
-// for doOpAdd and doOpAddAssign.
-func addAssign(alloc *Allocator, lv, rv *TypedValue) {
+// addAssign adds lv to rv and stores the result to lv.
+// It returns an exception in case of overflow on signed integers.
+// The assignement is performed even in case of exception.
+func addAssign(alloc *Allocator, lv, rv *TypedValue) *Exception {
// set the result in lv.
// NOTE this block is replicated in op_assign.go
+ ok := true
switch baseOf(lv.T) {
case StringType, UntypedStringType:
lv.V = alloc.NewString(lv.GetString() + rv.GetString())
+ // Signed integers may overflow, which triggers an exception.
case IntType:
- lv.SetInt(lv.GetInt() + rv.GetInt())
+ var r int
+ r, ok = overflow.Add(lv.GetInt(), rv.GetInt())
+ lv.SetInt(r)
case Int8Type:
- lv.SetInt8(lv.GetInt8() + rv.GetInt8())
+ var r int8
+ r, ok = overflow.Add8(lv.GetInt8(), rv.GetInt8())
+ lv.SetInt8(r)
case Int16Type:
- lv.SetInt16(lv.GetInt16() + rv.GetInt16())
+ var r int16
+ r, ok = overflow.Add16(lv.GetInt16(), rv.GetInt16())
+ lv.SetInt16(r)
case Int32Type, UntypedRuneType:
- lv.SetInt32(lv.GetInt32() + rv.GetInt32())
+ var r int32
+ r, ok = overflow.Add32(lv.GetInt32(), rv.GetInt32())
+ lv.SetInt32(r)
case Int64Type:
- lv.SetInt64(lv.GetInt64() + rv.GetInt64())
+ var r int64
+ r, ok = overflow.Add64(lv.GetInt64(), rv.GetInt64())
+ lv.SetInt64(r)
+ // Unsigned integers do not overflow, they just wrap.
case UintType:
lv.SetUint(lv.GetUint() + rv.GetUint())
case Uint8Type:
@@ -739,23 +757,42 @@ func addAssign(alloc *Allocator, lv, rv *TypedValue) {
lv.T,
))
}
+ if !ok {
+ return &Exception{Value: typedString("addition overflow")}
+ }
+ return nil
}
-// for doOpSub and doOpSubAssign.
-func subAssign(lv, rv *TypedValue) {
+// subAssign subtracts lv to rv and stores the result to lv.
+// It returns an exception in case of overflow on signed integers.
+// The subtraction is performed even in case of exception.
+func subAssign(lv, rv *TypedValue) *Exception {
// set the result in lv.
// NOTE this block is replicated in op_assign.go
+ ok := true
switch baseOf(lv.T) {
+ // Signed integers may overflow, which triggers an exception.
case IntType:
- lv.SetInt(lv.GetInt() - rv.GetInt())
+ var r int
+ r, ok = overflow.Sub(lv.GetInt(), rv.GetInt())
+ lv.SetInt(r)
case Int8Type:
- lv.SetInt8(lv.GetInt8() - rv.GetInt8())
+ var r int8
+ r, ok = overflow.Sub8(lv.GetInt8(), rv.GetInt8())
+ lv.SetInt8(r)
case Int16Type:
- lv.SetInt16(lv.GetInt16() - rv.GetInt16())
+ var r int16
+ r, ok = overflow.Sub16(lv.GetInt16(), rv.GetInt16())
+ lv.SetInt16(r)
case Int32Type, UntypedRuneType:
- lv.SetInt32(lv.GetInt32() - rv.GetInt32())
+ var r int32
+ r, ok = overflow.Sub32(lv.GetInt32(), rv.GetInt32())
+ lv.SetInt32(r)
case Int64Type:
- lv.SetInt64(lv.GetInt64() - rv.GetInt64())
+ var r int64
+ r, ok = overflow.Sub64(lv.GetInt64(), rv.GetInt64())
+ lv.SetInt64(r)
+ // Unsigned integers do not overflow, they just wrap.
case UintType:
lv.SetUint(lv.GetUint() - rv.GetUint())
case Uint8Type:
@@ -795,23 +832,39 @@ func subAssign(lv, rv *TypedValue) {
lv.T,
))
}
+ if !ok {
+ return &Exception{Value: typedString("subtraction overflow")}
+ }
+ return nil
}
// for doOpMul and doOpMulAssign.
-func mulAssign(lv, rv *TypedValue) {
+func mulAssign(lv, rv *TypedValue) *Exception {
// set the result in lv.
// NOTE this block is replicated in op_assign.go
+ ok := true
switch baseOf(lv.T) {
+ // Signed integers may overflow, which triggers a panic.
case IntType:
- lv.SetInt(lv.GetInt() * rv.GetInt())
+ var r int
+ r, ok = overflow.Mul(lv.GetInt(), rv.GetInt())
+ lv.SetInt(r)
case Int8Type:
- lv.SetInt8(lv.GetInt8() * rv.GetInt8())
+ var r int8
+ r, ok = overflow.Mul8(lv.GetInt8(), rv.GetInt8())
+ lv.SetInt8(r)
case Int16Type:
- lv.SetInt16(lv.GetInt16() * rv.GetInt16())
+ var r int16
+ r, ok = overflow.Mul16(lv.GetInt16(), rv.GetInt16())
+ lv.SetInt16(r)
case Int32Type, UntypedRuneType:
- lv.SetInt32(lv.GetInt32() * rv.GetInt32())
+ var r int32
+ r, ok = overflow.Mul32(lv.GetInt32(), rv.GetInt32())
+ lv.SetInt32(r)
case Int64Type:
- lv.SetInt64(lv.GetInt64() * rv.GetInt64())
+ var r int64
+ r, ok = overflow.Mul64(lv.GetInt64(), rv.GetInt64())
+ lv.SetInt64(r)
case UintType:
lv.SetUint(lv.GetUint() * rv.GetUint())
case Uint8Type:
@@ -849,96 +902,105 @@ func mulAssign(lv, rv *TypedValue) {
lv.T,
))
}
+ if !ok {
+ return &Exception{Value: typedString("multiplication overflow")}
+ }
+ return nil
}
// for doOpQuo and doOpQuoAssign.
func quoAssign(lv, rv *TypedValue) *Exception {
- expt := &Exception{
- Value: typedString("division by zero"),
- }
-
// set the result in lv.
// NOTE this block is replicated in op_assign.go
+ ok := true
switch baseOf(lv.T) {
+ // Signed integers may overflow or cause a division by 0, which triggers a panic.
case IntType:
- if rv.GetInt() == 0 {
- return expt
- }
- lv.SetInt(lv.GetInt() / rv.GetInt())
+ var q int
+ q, _, ok = overflow.Quotient(lv.GetInt(), rv.GetInt())
+ lv.SetInt(q)
case Int8Type:
- if rv.GetInt8() == 0 {
- return expt
- }
- lv.SetInt8(lv.GetInt8() / rv.GetInt8())
+ var q int8
+ q, _, ok = overflow.Quotient8(lv.GetInt8(), rv.GetInt8())
+ lv.SetInt8(q)
case Int16Type:
- if rv.GetInt16() == 0 {
- return expt
- }
- lv.SetInt16(lv.GetInt16() / rv.GetInt16())
+ var q int16
+ q, _, ok = overflow.Quotient16(lv.GetInt16(), rv.GetInt16())
+ lv.SetInt16(q)
case Int32Type, UntypedRuneType:
- if rv.GetInt32() == 0 {
- return expt
- }
- lv.SetInt32(lv.GetInt32() / rv.GetInt32())
+ var q int32
+ q, _, ok = overflow.Quotient32(lv.GetInt32(), rv.GetInt32())
+ lv.SetInt32(q)
case Int64Type:
- if rv.GetInt64() == 0 {
- return expt
- }
- lv.SetInt64(lv.GetInt64() / rv.GetInt64())
+ var q int64
+ q, _, ok = overflow.Quotient64(lv.GetInt64(), rv.GetInt64())
+ lv.SetInt64(q)
+ // Unsigned integers do not cause overflow, but a division by 0 may still occur.
case UintType:
- if rv.GetUint() == 0 {
- return expt
+ y := rv.GetUint()
+ ok = y != 0
+ if ok {
+ lv.SetUint(lv.GetUint() / y)
}
- lv.SetUint(lv.GetUint() / rv.GetUint())
case Uint8Type:
- if rv.GetUint8() == 0 {
- return expt
+ y := rv.GetUint8()
+ ok = y != 0
+ if ok {
+ lv.SetUint8(lv.GetUint8() / y)
}
- lv.SetUint8(lv.GetUint8() / rv.GetUint8())
case DataByteType:
- if rv.GetUint8() == 0 {
- return expt
+ y := rv.GetUint8()
+ ok = y != 0
+ if ok {
+ lv.SetDataByte(lv.GetDataByte() / y)
}
- lv.SetDataByte(lv.GetDataByte() / rv.GetUint8())
case Uint16Type:
- if rv.GetUint16() == 0 {
- return expt
+ y := rv.GetUint16()
+ ok = y != 0
+ if ok {
+ lv.SetUint16(lv.GetUint16() / y)
}
- lv.SetUint16(lv.GetUint16() / rv.GetUint16())
case Uint32Type:
- if rv.GetUint32() == 0 {
- return expt
+ y := rv.GetUint32()
+ ok = y != 0
+ if ok {
+ lv.SetUint32(lv.GetUint32() / y)
}
- lv.SetUint32(lv.GetUint32() / rv.GetUint32())
case Uint64Type:
- if rv.GetUint64() == 0 {
- return expt
+ y := rv.GetUint64()
+ ok = y != 0
+ if ok {
+ lv.SetUint64(lv.GetUint64() / y)
}
- lv.SetUint64(lv.GetUint64() / rv.GetUint64())
+ // XXX Handling float overflows is more complex.
case Float32Type:
// NOTE: gno doesn't fuse *+.
- if rv.GetFloat32() == 0 {
- return expt
+ y := rv.GetFloat32()
+ ok = y != 0
+ if ok {
+ lv.SetFloat32(lv.GetFloat32() / y)
}
- lv.SetFloat32(lv.GetFloat32() / rv.GetFloat32())
// XXX FOR DETERMINISM, PANIC IF NAN.
case Float64Type:
// NOTE: gno doesn't fuse *+.
- if rv.GetFloat64() == 0 {
- return expt
+ y := rv.GetFloat64()
+ ok = y != 0
+ if ok {
+ lv.SetFloat64(lv.GetFloat64() / y)
}
- lv.SetFloat64(lv.GetFloat64() / rv.GetFloat64())
// XXX FOR DETERMINISM, PANIC IF NAN.
case BigintType, UntypedBigintType:
if rv.GetBigInt().Sign() == 0 {
- return expt
+ ok = false
+ break
}
lb := lv.GetBigInt()
lb = big.NewInt(0).Quo(lb, rv.GetBigInt())
lv.V = BigintValue{V: lb}
case BigdecType, UntypedBigdecType:
if rv.GetBigDec().Cmp(apd.New(0, 0)) == 0 {
- return expt
+ ok = false
+ break
}
lb := lv.GetBigDec()
rb := rv.GetBigDec()
@@ -955,81 +1017,83 @@ func quoAssign(lv, rv *TypedValue) *Exception {
))
}
+ if !ok {
+ return &Exception{Value: typedString("division by zero or overflow")}
+ }
return nil
}
// for doOpRem and doOpRemAssign.
func remAssign(lv, rv *TypedValue) *Exception {
- expt := &Exception{
- Value: typedString("division by zero"),
- }
-
// set the result in lv.
// NOTE this block is replicated in op_assign.go
+ ok := true
switch baseOf(lv.T) {
+ // Signed integers may overflow or cause a division by 0, which triggers a panic.
case IntType:
- if rv.GetInt() == 0 {
- return expt
- }
- lv.SetInt(lv.GetInt() % rv.GetInt())
+ var r int
+ _, r, ok = overflow.Quotient(lv.GetInt(), rv.GetInt())
+ lv.SetInt(r)
case Int8Type:
- if rv.GetInt8() == 0 {
- return expt
- }
- lv.SetInt8(lv.GetInt8() % rv.GetInt8())
+ var r int8
+ _, r, ok = overflow.Quotient8(lv.GetInt8(), rv.GetInt8())
+ lv.SetInt8(r)
case Int16Type:
- if rv.GetInt16() == 0 {
- return expt
- }
- lv.SetInt16(lv.GetInt16() % rv.GetInt16())
+ var r int16
+ _, r, ok = overflow.Quotient16(lv.GetInt16(), rv.GetInt16())
+ lv.SetInt16(r)
case Int32Type, UntypedRuneType:
- if rv.GetInt32() == 0 {
- return expt
- }
- lv.SetInt32(lv.GetInt32() % rv.GetInt32())
+ var r int32
+ _, r, ok = overflow.Quotient32(lv.GetInt32(), rv.GetInt32())
+ lv.SetInt32(r)
case Int64Type:
- if rv.GetInt64() == 0 {
- return expt
- }
- lv.SetInt64(lv.GetInt64() % rv.GetInt64())
+ var r int64
+ _, r, ok = overflow.Quotient64(lv.GetInt64(), rv.GetInt64())
+ lv.SetInt64(r)
+ // Unsigned integers do not cause overflow, but a division by 0 may still occur.
case UintType:
- if rv.GetUint() == 0 {
- return expt
+ y := rv.GetUint()
+ ok = y != 0
+ if ok {
+ lv.SetUint(lv.GetUint() % y)
}
- lv.SetUint(lv.GetUint() % rv.GetUint())
case Uint8Type:
- if rv.GetUint8() == 0 {
- return expt
+ y := rv.GetUint8()
+ ok = y != 0
+ if ok {
+ lv.SetUint8(lv.GetUint8() % y)
}
- lv.SetUint8(lv.GetUint8() % rv.GetUint8())
case DataByteType:
- if rv.GetUint8() == 0 {
- return expt
+ y := rv.GetUint8()
+ ok = y != 0
+ if ok {
+ lv.SetDataByte(lv.GetDataByte() % y)
}
- lv.SetDataByte(lv.GetDataByte() % rv.GetUint8())
case Uint16Type:
- if rv.GetUint16() == 0 {
- return expt
+ y := rv.GetUint16()
+ ok = y != 0
+ if ok {
+ lv.SetUint16(lv.GetUint16() % y)
}
- lv.SetUint16(lv.GetUint16() % rv.GetUint16())
case Uint32Type:
- if rv.GetUint32() == 0 {
- return expt
+ y := rv.GetUint32()
+ ok = y != 0
+ if ok {
+ lv.SetUint32(lv.GetUint32() % y)
}
- lv.SetUint32(lv.GetUint32() % rv.GetUint32())
case Uint64Type:
- if rv.GetUint64() == 0 {
- return expt
+ y := rv.GetUint64()
+ ok = y != 0
+ if ok {
+ lv.SetUint64(lv.GetUint64() % y)
}
- lv.SetUint64(lv.GetUint64() % rv.GetUint64())
case BigintType, UntypedBigintType:
- if rv.GetBigInt().Sign() == 0 {
- return expt
+ ok = rv.GetBigInt().Sign() != 0
+ if ok {
+ lb := lv.GetBigInt()
+ lb = big.NewInt(0).Rem(lb, rv.GetBigInt())
+ lv.V = BigintValue{V: lb}
}
-
- lb := lv.GetBigInt()
- lb = big.NewInt(0).Rem(lb, rv.GetBigInt())
- lv.V = BigintValue{V: lb}
default:
panic(fmt.Sprintf(
"operators %% and %%= not defined for %s",
@@ -1037,6 +1101,9 @@ func remAssign(lv, rv *TypedValue) *Exception {
))
}
+ if !ok {
+ return &Exception{Value: typedString("division by zero or overflow")}
+ }
return nil
}
diff --git a/gnovm/pkg/gnolang/op_inc_dec.go b/gnovm/pkg/gnolang/op_inc_dec.go
index 7a8a885bcf0..1e68e195596 100644
--- a/gnovm/pkg/gnolang/op_inc_dec.go
+++ b/gnovm/pkg/gnolang/op_inc_dec.go
@@ -5,6 +5,7 @@ import (
"math/big"
"github.com/cockroachdb/apd/v3"
+ "github.com/gnolang/gno/tm2/pkg/overflow"
)
func (m *Machine) doOpInc() {
@@ -31,16 +32,18 @@ func (m *Machine) doOpInc() {
// because it could be a type alias
// type num int
switch baseOf(lv.T) {
+ // Signed integers may overflow, which triggers a panic.
case IntType:
- lv.SetInt(lv.GetInt() + 1)
+ lv.SetInt(overflow.Addp(lv.GetInt(), 1))
case Int8Type:
- lv.SetInt8(lv.GetInt8() + 1)
+ lv.SetInt8(overflow.Add8p(lv.GetInt8(), 1))
case Int16Type:
- lv.SetInt16(lv.GetInt16() + 1)
+ lv.SetInt16(overflow.Add16p(lv.GetInt16(), 1))
case Int32Type:
- lv.SetInt32(lv.GetInt32() + 1)
+ lv.SetInt32(overflow.Add32p(lv.GetInt32(), 1))
case Int64Type:
- lv.SetInt64(lv.GetInt64() + 1)
+ lv.SetInt64(overflow.Add64p(lv.GetInt64(), 1))
+ // Unsigned integers do not overflow, they just wrap.
case UintType:
lv.SetUint(lv.GetUint() + 1)
case Uint8Type:
@@ -101,16 +104,18 @@ func (m *Machine) doOpDec() {
}
}
switch baseOf(lv.T) {
+ // Signed integers may overflow, which triggers a panic.
case IntType:
- lv.SetInt(lv.GetInt() - 1)
+ lv.SetInt(overflow.Subp(lv.GetInt(), 1))
case Int8Type:
- lv.SetInt8(lv.GetInt8() - 1)
+ lv.SetInt8(overflow.Sub8p(lv.GetInt8(), 1))
case Int16Type:
- lv.SetInt16(lv.GetInt16() - 1)
+ lv.SetInt16(overflow.Sub16p(lv.GetInt16(), 1))
case Int32Type:
- lv.SetInt32(lv.GetInt32() - 1)
+ lv.SetInt32(overflow.Sub32p(lv.GetInt32(), 1))
case Int64Type:
- lv.SetInt64(lv.GetInt64() - 1)
+ lv.SetInt64(overflow.Sub64p(lv.GetInt64(), 1))
+ // Unsigned integers do not overflow, they just wrap.
case UintType:
lv.SetUint(lv.GetUint() - 1)
case Uint8Type:
diff --git a/gnovm/pkg/gnolang/realm.go b/gnovm/pkg/gnolang/realm.go
index d822eb290eb..04de760037a 100644
--- a/gnovm/pkg/gnolang/realm.go
+++ b/gnovm/pkg/gnolang/realm.go
@@ -6,6 +6,7 @@ import (
"fmt"
"reflect"
"strings"
+ "sync"
bm "github.com/gnolang/gno/gnovm/pkg/benchops"
)
@@ -71,8 +72,25 @@ func (pid PkgID) Bytes() []byte {
return pid.Hashlet[:]
}
+var (
+ pkgIDFromPkgPathCacheMu sync.Mutex // protects the shared cache.
+ // TODO: later on switch this to an LRU if needed to ensure
+ // fixed memory caps. For now though it isn't a problem:
+ // https://github.com/gnolang/gno/pull/3424#issuecomment-2564571785
+ pkgIDFromPkgPathCache = make(map[string]*PkgID, 100)
+)
+
func PkgIDFromPkgPath(path string) PkgID {
- return PkgID{HashBytes([]byte(path))}
+ pkgIDFromPkgPathCacheMu.Lock()
+ defer pkgIDFromPkgPathCacheMu.Unlock()
+
+ pkgID, ok := pkgIDFromPkgPathCache[path]
+ if !ok {
+ pkgID = new(PkgID)
+ *pkgID = PkgID{HashBytes([]byte(path))}
+ pkgIDFromPkgPathCache[path] = pkgID
+ }
+ return *pkgID
}
// Returns the ObjectID of the PackageValue associated with path.
diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/a.go b/gnovm/pkg/gnolang/testdata/corpra/parsefile/a.go
new file mode 100644
index 00000000000..ae05a655fd7
--- /dev/null
+++ b/gnovm/pkg/gnolang/testdata/corpra/parsefile/a.go
@@ -0,0 +1,9 @@
+package main
+
+import
+ _ "math/big"
+)
+
+func main() {
+ println("Foo")
+}
diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/b.go b/gnovm/pkg/gnolang/testdata/corpra/parsefile/b.go
new file mode 100644
index 00000000000..07592ff47ed
--- /dev/null
+++ b/gnovm/pkg/gnolang/testdata/corpra/parsefile/b.go
@@ -0,0 +1,16 @@
+package main
+
+import "crypto/rand"
+
+func init() {
+}
+
+func init() {
+}
+
+func init() {
+}
+
+func it() {
+ _ = rand.Read
+}
diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3013.go b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3013.go
new file mode 100644
index 00000000000..f664f68f1b6
--- /dev/null
+++ b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3013.go
@@ -0,0 +1,22 @@
+package main
+
+import "testing"
+
+func TestDummy(t *testing.T) {
+ testTable := []struct {
+ name string
+ }{
+ {
+ "one",
+ },
+ {
+ "two",
+ },
+ }
+
+ for _, testCase := range testTable {
+ testCase := testCase
+
+ println(testCase.name)
+ }
+}
diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine.go b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine.go
new file mode 100644
index 00000000000..877b5fafc1d
--- /dev/null
+++ b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine.go
@@ -0,0 +1,10 @@
+package main
+οΏΌ
+οΏΌvar ss = []int{1, 2, 3}
+οΏΌ
+οΏΌfunc main() {
+οΏΌ for _, s := range ss {
+οΏΌ s := s
+οΏΌ println(s)
+οΏΌ }
+οΏΌ}
diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine2.go b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine2.go
new file mode 100644
index 00000000000..450406a2202
--- /dev/null
+++ b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine2.go
@@ -0,0 +1,21 @@
+package main
+οΏΌ
+οΏΌvar testTable = []struct {
+οΏΌ name string
+οΏΌ}{
+οΏΌ {
+οΏΌ "one",
+οΏΌ },
+οΏΌ {
+οΏΌ "two",
+οΏΌ },
+οΏΌ}
+οΏΌ
+οΏΌfunc main() {
+οΏΌ
+οΏΌ for _, testCase := range testTable {
+οΏΌ testCase := testCase
+οΏΌ
+οΏΌ println(testCase.name)
+οΏΌ }
+οΏΌ}
diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine3.go b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine3.go
new file mode 100644
index 00000000000..74ed729fb28
--- /dev/null
+++ b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine3.go
@@ -0,0 +1,8 @@
+package main
+οΏΌ
+οΏΌfunc main() {
+οΏΌ for i := 0; i < 3; i++ {
+οΏΌ i := i
+οΏΌ println(i)
+οΏΌ }
+οΏΌ}
diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine4.go b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine4.go
new file mode 100644
index 00000000000..db27dcdc6bf
--- /dev/null
+++ b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine4.go
@@ -0,0 +1,11 @@
+package main
+οΏΌ
+οΏΌfunc main() {
+οΏΌ a := 1
+οΏΌ b := 3
+οΏΌ println(a, b) // prints 1 3
+οΏΌ
+οΏΌ // Re-declaration of 'a' is allowed because 'c' is a new variable
+οΏΌ a, c := 2, 5
+οΏΌ println(a, c) // prints 2 5
+οΏΌ}
diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine5.go b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine5.go
new file mode 100644
index 00000000000..97ec50f3330
--- /dev/null
+++ b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine5.go
@@ -0,0 +1,13 @@
+package main
+οΏΌ
+οΏΌfunc main() {
+οΏΌ a := 1
+οΏΌ println(a) // prints 1
+οΏΌ
+οΏΌ if true {
+οΏΌ a := 2 // valid: new scope inside the if statement
+οΏΌ println(a) // prints 2
+οΏΌ }
+οΏΌ
+οΏΌ println(a) // prints 1: outer variable is unchanged
+οΏΌ}
diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine6.go b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine6.go
new file mode 100644
index 00000000000..49b871be9c0
--- /dev/null
+++ b/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine6.go
@@ -0,0 +1,6 @@
+package main
+οΏΌ
+οΏΌfunc main() {
+οΏΌ a, b := 1, 2
+οΏΌ a, b := 3, 4
+οΏΌ}
diff --git a/gnovm/pkg/gnolang/testdata/fuzz/FuzzConvertUntypedBigdecToFloat/4be24841138e3224 b/gnovm/pkg/gnolang/testdata/fuzz/FuzzConvertUntypedBigdecToFloat/4be24841138e3224
new file mode 100644
index 00000000000..8894efa8d8e
--- /dev/null
+++ b/gnovm/pkg/gnolang/testdata/fuzz/FuzzConvertUntypedBigdecToFloat/4be24841138e3224
@@ -0,0 +1,2 @@
+go test fuzz v1
+string(".-700000000000000000000000000000000000000")
diff --git a/gnovm/pkg/gnolang/testdata/fuzz/FuzzConvertUntypedBigdecToFloat/94196a9456e79dac b/gnovm/pkg/gnolang/testdata/fuzz/FuzzConvertUntypedBigdecToFloat/94196a9456e79dac
new file mode 100644
index 00000000000..a317fbab107
--- /dev/null
+++ b/gnovm/pkg/gnolang/testdata/fuzz/FuzzConvertUntypedBigdecToFloat/94196a9456e79dac
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
diff --git a/gnovm/pkg/packages/filekind_test.go b/gnovm/pkg/packages/filekind_test.go
index c5a18ee38ed..bd06b49fb45 100644
--- a/gnovm/pkg/packages/filekind_test.go
+++ b/gnovm/pkg/packages/filekind_test.go
@@ -1,9 +1,8 @@
-package packages_test
+package packages
import (
"testing"
- . "github.com/gnolang/gno/gnovm/pkg/packages"
"github.com/stretchr/testify/require"
)
diff --git a/gnovm/pkg/transpiler/fuzz_test.go b/gnovm/pkg/transpiler/fuzz_test.go
new file mode 100644
index 00000000000..d3ab26eea86
--- /dev/null
+++ b/gnovm/pkg/transpiler/fuzz_test.go
@@ -0,0 +1,76 @@
+package transpiler_test
+
+import (
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/gnolang/gno/gnovm/pkg/gnoenv"
+ "github.com/gnolang/gno/gnovm/pkg/transpiler"
+)
+
+func FuzzTranspiling(f *testing.F) {
+ if testing.Short() {
+ f.Skip("Running in -short mode")
+ }
+
+ // 1. Derive the seeds from our seedGnoFiles.
+ ffs := os.DirFS(filepath.Join(gnoenv.RootDir(), "examples"))
+ fs.WalkDir(ffs, ".", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ panic(err)
+ }
+ if !strings.HasSuffix(path, ".gno") {
+ return nil
+ }
+ file, err := ffs.Open(path)
+ if err != nil {
+ panic(err)
+ }
+ blob, err := io.ReadAll(file)
+ file.Close()
+ if err != nil {
+ panic(err)
+ }
+ f.Add(blob)
+ return nil
+ })
+
+ // 2. Run the fuzzers.
+ f.Fuzz(func(t *testing.T, gnoSourceCode []byte) {
+ // 3. Add timings to ensure that if transpiling takes a long time
+ // to run, that we report this as problematic.
+ doneCh := make(chan bool, 1)
+ readyCh := make(chan bool)
+ go func() {
+ defer func() {
+ r := recover()
+ if r == nil {
+ return
+ }
+
+ sr := fmt.Sprintf("%s", r)
+ if !strings.Contains(sr, "invalid line number ") {
+ panic(r)
+ }
+ }()
+ close(readyCh)
+ defer close(doneCh)
+ _, _ = transpiler.Transpile(string(gnoSourceCode), "gno", "in.gno")
+ doneCh <- true
+ }()
+
+ <-readyCh
+
+ select {
+ case <-time.After(2 * time.Second):
+ t.Fatalf("took more than 2 seconds to transpile\n\n%s", gnoSourceCode)
+ case <-doneCh:
+ }
+ })
+}
diff --git a/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/26ec2c0a22e242fc b/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/26ec2c0a22e242fc
new file mode 100644
index 00000000000..d82acb3eaa5
--- /dev/null
+++ b/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/26ec2c0a22e242fc
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("package\tA\nimport(\"\"//\"\n\"\"/***/)")
diff --git a/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/54f7287f473abfa5 b/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/54f7287f473abfa5
new file mode 100644
index 00000000000..a3accb3dd8e
--- /dev/null
+++ b/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/54f7287f473abfa5
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("package A\nimport(\"\"//\"\n\"\"/***/)")
diff --git a/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/703a1cacabb84c6d b/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/703a1cacabb84c6d
new file mode 100644
index 00000000000..c08c7cfe904
--- /dev/null
+++ b/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/703a1cacabb84c6d
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("package A\ncon\x12\n\xec|b\x80\xddQst(\n/*\n\n\n\n\n\n\nka\n*/A)")
diff --git a/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/bcd60839c81ca478 b/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/bcd60839c81ca478
new file mode 100644
index 00000000000..df9d3b1b5b9
--- /dev/null
+++ b/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/bcd60839c81ca478
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("//\"\npackage\tA\nimport(\"\"//\"\n\"\"/***/)")
diff --git a/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/bf4f7515b25bf71b b/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/bf4f7515b25bf71b
new file mode 100644
index 00000000000..cbc2bd9be5b
--- /dev/null
+++ b/gnovm/pkg/transpiler/testdata/fuzz/FuzzTranspiling/bf4f7515b25bf71b
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("//0000\x170000000000:0\npackage A")
diff --git a/gnovm/stdlibs/generated.go b/gnovm/stdlibs/generated.go
index d26af7d3b00..5201d1d6fe4 100644
--- a/gnovm/stdlibs/generated.go
+++ b/gnovm/stdlibs/generated.go
@@ -1033,7 +1033,6 @@ var initOrder = [...]string{
"hash",
"hash/adler32",
"html",
- "math/overflow",
"math/rand",
"path",
"sort",
diff --git a/gnovm/stdlibs/math/const_test.gno b/gnovm/stdlibs/math/const_test.gno
index b892a12898b..fbe59d61878 100644
--- a/gnovm/stdlibs/math/const_test.gno
+++ b/gnovm/stdlibs/math/const_test.gno
@@ -31,19 +31,76 @@ func TestMaxUint(t *testing.T) {
}
func TestMaxInt(t *testing.T) {
- if v := int(math.MaxInt); v+1 != math.MinInt {
- t.Errorf("MaxInt should wrap around to MinInt: %d", v+1)
+ defer func() {
+ if r := recover(); r != nil {
+ if r != "addition overflow" {
+ panic(r)
+ }
+ }
+ }()
+ v := int(math.MaxInt)
+ if v+1 == math.MinInt {
+ t.Errorf("int should overflow")
}
- if v := int8(math.MaxInt8); v+1 != math.MinInt8 {
- t.Errorf("MaxInt8 should wrap around to MinInt8: %d", v+1)
+ t.Errorf("expected panic did not occur")
+}
+
+func TestMaxInt8(t *testing.T) {
+ defer func() {
+ if r := recover(); r != nil {
+ if r != "addition overflow" {
+ panic(r)
+ }
+ }
+ }()
+ v := int8(math.MaxInt8)
+ if v+1 == math.MinInt8 {
+ t.Errorf("int8 should overflow")
}
- if v := int16(math.MaxInt16); v+1 != math.MinInt16 {
- t.Errorf("MaxInt16 should wrap around to MinInt16: %d", v+1)
+ t.Errorf("expected panic did not occur")
+}
+
+func TestMaxInt16(t *testing.T) {
+ defer func() {
+ if r := recover(); r != nil {
+ if r != "addition overflow" {
+ panic(r)
+ }
+ }
+ }()
+ v := int16(math.MaxInt16)
+ if v+1 == math.MinInt16 {
+ t.Errorf("int16 should overflow")
}
- if v := int32(math.MaxInt32); v+1 != math.MinInt32 {
- t.Errorf("MaxInt32 should wrap around to MinInt32: %d", v+1)
+ t.Errorf("expected panic did not occur")
+}
+
+func TestMaxInt32(t *testing.T) {
+ defer func() {
+ if r := recover(); r != nil {
+ if r != "addition overflow" {
+ panic(r)
+ }
+ }
+ }()
+ v := int32(math.MaxInt32)
+ if v+1 == math.MinInt32 {
+ t.Errorf("int32 should overflow")
}
- if v := int64(math.MaxInt64); v+1 != math.MinInt64 {
- t.Errorf("MaxInt64 should wrap around to MinInt64: %d", v+1)
+ t.Errorf("expected panic did not occur")
+}
+
+func TestMaxInt64(t *testing.T) {
+ defer func() {
+ if r := recover(); r != nil {
+ if r != "addition overflow" {
+ panic(r)
+ }
+ }
+ }()
+ v := int64(math.MaxInt64)
+ if v+1 == math.MinInt64 {
+ t.Errorf("int64 should overflow")
}
+ t.Errorf("expected panic did not occur")
}
diff --git a/gnovm/stdlibs/math/overflow/overflow.gno b/gnovm/stdlibs/math/overflow/overflow.gno
deleted file mode 100644
index 0bc2e03a522..00000000000
--- a/gnovm/stdlibs/math/overflow/overflow.gno
+++ /dev/null
@@ -1,501 +0,0 @@
-// This is modified from https://github.com/JohnCGriffin/overflow (MIT).
-// NOTE: there was a bug with the original Quotient* functions, and
-// testing method. These have been fixed here, and tests ported to
-// tests/files/maths_int*.go respectively.
-// Note: moved over from p/demo/maths.
-
-/*
-Package overflow offers overflow-checked integer arithmetic operations
-for int, int32, and int64. Each of the operations returns a
-result,bool combination. This was prompted by the need to know when
-to flow into higher precision types from the math.big library.
-
-For instance, assuing a 64 bit machine:
-
-10 + 20 -> 30
-int(math.MaxInt64) + 1 -> -9223372036854775808
-
-whereas
-
-overflow.Add(10,20) -> (30, true)
-overflow.Add(math.MaxInt64,1) -> (0, false)
-
-Add, Sub, Mul, Div are for int. Add64, Add32, etc. are specifically sized.
-
-If anybody wishes an unsigned version, submit a pull request for code
-and new tests.
-*/
-package overflow
-
-import "math"
-
-//go:generate ./overflow_template.sh
-
-func _is64Bit() bool {
- maxU32 := uint(math.MaxUint32)
- return ((maxU32 << 1) >> 1) == maxU32
-}
-
-/********** PARTIAL TEST COVERAGE FROM HERE DOWN *************
-
-The only way that I could see to do this is a combination of
-my normal 64 bit system and a GopherJS running on Node. My
-understanding is that its ints are 32 bit.
-
-So, FEEL FREE to carefully review the code visually.
-
-*************************************************************/
-
-// Unspecified size, i.e. normal signed int
-
-// Add sums two ints, returning the result and a boolean status.
-func Add(a, b int) (int, bool) {
- if _is64Bit() {
- r64, ok := Add64(int64(a), int64(b))
- return int(r64), ok
- }
- r32, ok := Add32(int32(a), int32(b))
- return int(r32), ok
-}
-
-// Sub returns the difference of two ints and a boolean status.
-func Sub(a, b int) (int, bool) {
- if _is64Bit() {
- r64, ok := Sub64(int64(a), int64(b))
- return int(r64), ok
- }
- r32, ok := Sub32(int32(a), int32(b))
- return int(r32), ok
-}
-
-// Mul returns the product of two ints and a boolean status.
-func Mul(a, b int) (int, bool) {
- if _is64Bit() {
- r64, ok := Mul64(int64(a), int64(b))
- return int(r64), ok
- }
- r32, ok := Mul32(int32(a), int32(b))
- return int(r32), ok
-}
-
-// Div returns the quotient of two ints and a boolean status
-func Div(a, b int) (int, bool) {
- if _is64Bit() {
- r64, ok := Div64(int64(a), int64(b))
- return int(r64), ok
- }
- r32, ok := Div32(int32(a), int32(b))
- return int(r32), ok
-}
-
-// Quo returns the quotient, remainder and status of two ints
-func Quo(a, b int) (int, int, bool) {
- if _is64Bit() {
- q64, r64, ok := Quo64(int64(a), int64(b))
- return int(q64), int(r64), ok
- }
- q32, r32, ok := Quo32(int32(a), int32(b))
- return int(q32), int(r32), ok
-}
-
-/************* Panic versions for int ****************/
-
-// Addp returns the sum of two ints, panicking on overflow
-func Addp(a, b int) int {
- r, ok := Add(a, b)
- if !ok {
- panic("addition overflow")
- }
- return r
-}
-
-// Subp returns the difference of two ints, panicking on overflow.
-func Subp(a, b int) int {
- r, ok := Sub(a, b)
- if !ok {
- panic("subtraction overflow")
- }
- return r
-}
-
-// Mulp returns the product of two ints, panicking on overflow.
-func Mulp(a, b int) int {
- r, ok := Mul(a, b)
- if !ok {
- panic("multiplication overflow")
- }
- return r
-}
-
-// Divp returns the quotient of two ints, panicking on overflow.
-func Divp(a, b int) int {
- r, ok := Div(a, b)
- if !ok {
- panic("division failure")
- }
- return r
-}
-
-//----------------------------------------
-// This is generated code, created by overflow_template.sh executed
-// by "go generate"
-
-// Add8 performs + operation on two int8 operands
-// returning a result and status
-func Add8(a, b int8) (int8, bool) {
- c := a + b
- if (c > a) == (b > 0) {
- return c, true
- }
- return c, false
-}
-
-// Add8p is the unchecked panicking version of Add8
-func Add8p(a, b int8) int8 {
- r, ok := Add8(a, b)
- if !ok {
- panic("addition overflow")
- }
- return r
-}
-
-// Sub8 performs - operation on two int8 operands
-// returning a result and status
-func Sub8(a, b int8) (int8, bool) {
- c := a - b
- if (c < a) == (b > 0) {
- return c, true
- }
- return c, false
-}
-
-// Sub8p is the unchecked panicking version of Sub8
-func Sub8p(a, b int8) int8 {
- r, ok := Sub8(a, b)
- if !ok {
- panic("subtraction overflow")
- }
- return r
-}
-
-// Mul8 performs * operation on two int8 operands
-// returning a result and status
-func Mul8(a, b int8) (int8, bool) {
- if a == 0 || b == 0 {
- return 0, true
- }
- c := a * b
- if (c < 0) == ((a < 0) != (b < 0)) {
- if c/b == a {
- return c, true
- }
- }
- return c, false
-}
-
-// Mul8p is the unchecked panicking version of Mul8
-func Mul8p(a, b int8) int8 {
- r, ok := Mul8(a, b)
- if !ok {
- panic("multiplication overflow")
- }
- return r
-}
-
-// Div8 performs / operation on two int8 operands
-// returning a result and status
-func Div8(a, b int8) (int8, bool) {
- q, _, ok := Quo8(a, b)
- return q, ok
-}
-
-// Div8p is the unchecked panicking version of Div8
-func Div8p(a, b int8) int8 {
- r, ok := Div8(a, b)
- if !ok {
- panic("division failure")
- }
- return r
-}
-
-// Quo8 performs + operation on two int8 operands
-// returning a quotient, a remainder and status
-func Quo8(a, b int8) (int8, int8, bool) {
- if b == 0 {
- return 0, 0, false
- } else if b == -1 && a == int8(math.MinInt8) {
- return 0, 0, false
- }
- c := a / b
- return c, a % b, true
-}
-
-// Add16 performs + operation on two int16 operands
-// returning a result and status
-func Add16(a, b int16) (int16, bool) {
- c := a + b
- if (c > a) == (b > 0) {
- return c, true
- }
- return c, false
-}
-
-// Add16p is the unchecked panicking version of Add16
-func Add16p(a, b int16) int16 {
- r, ok := Add16(a, b)
- if !ok {
- panic("addition overflow")
- }
- return r
-}
-
-// Sub16 performs - operation on two int16 operands
-// returning a result and status
-func Sub16(a, b int16) (int16, bool) {
- c := a - b
- if (c < a) == (b > 0) {
- return c, true
- }
- return c, false
-}
-
-// Sub16p is the unchecked panicking version of Sub16
-func Sub16p(a, b int16) int16 {
- r, ok := Sub16(a, b)
- if !ok {
- panic("subtraction overflow")
- }
- return r
-}
-
-// Mul16 performs * operation on two int16 operands
-// returning a result and status
-func Mul16(a, b int16) (int16, bool) {
- if a == 0 || b == 0 {
- return 0, true
- }
- c := a * b
- if (c < 0) == ((a < 0) != (b < 0)) {
- if c/b == a {
- return c, true
- }
- }
- return c, false
-}
-
-// Mul16p is the unchecked panicking version of Mul16
-func Mul16p(a, b int16) int16 {
- r, ok := Mul16(a, b)
- if !ok {
- panic("multiplication overflow")
- }
- return r
-}
-
-// Div16 performs / operation on two int16 operands
-// returning a result and status
-func Div16(a, b int16) (int16, bool) {
- q, _, ok := Quo16(a, b)
- return q, ok
-}
-
-// Div16p is the unchecked panicking version of Div16
-func Div16p(a, b int16) int16 {
- r, ok := Div16(a, b)
- if !ok {
- panic("division failure")
- }
- return r
-}
-
-// Quo16 performs + operation on two int16 operands
-// returning a quotient, a remainder and status
-func Quo16(a, b int16) (int16, int16, bool) {
- if b == 0 {
- return 0, 0, false
- } else if b == -1 && a == int16(math.MinInt16) {
- return 0, 0, false
- }
- c := a / b
- return c, a % b, true
-}
-
-// Add32 performs + operation on two int32 operands
-// returning a result and status
-func Add32(a, b int32) (int32, bool) {
- c := a + b
- if (c > a) == (b > 0) {
- return c, true
- }
- return c, false
-}
-
-// Add32p is the unchecked panicking version of Add32
-func Add32p(a, b int32) int32 {
- r, ok := Add32(a, b)
- if !ok {
- panic("addition overflow")
- }
- return r
-}
-
-// Sub32 performs - operation on two int32 operands
-// returning a result and status
-func Sub32(a, b int32) (int32, bool) {
- c := a - b
- if (c < a) == (b > 0) {
- return c, true
- }
- return c, false
-}
-
-// Sub32p is the unchecked panicking version of Sub32
-func Sub32p(a, b int32) int32 {
- r, ok := Sub32(a, b)
- if !ok {
- panic("subtraction overflow")
- }
- return r
-}
-
-// Mul32 performs * operation on two int32 operands
-// returning a result and status
-func Mul32(a, b int32) (int32, bool) {
- if a == 0 || b == 0 {
- return 0, true
- }
- c := a * b
- if (c < 0) == ((a < 0) != (b < 0)) {
- if c/b == a {
- return c, true
- }
- }
- return c, false
-}
-
-// Mul32p is the unchecked panicking version of Mul32
-func Mul32p(a, b int32) int32 {
- r, ok := Mul32(a, b)
- if !ok {
- panic("multiplication overflow")
- }
- return r
-}
-
-// Div32 performs / operation on two int32 operands
-// returning a result and status
-func Div32(a, b int32) (int32, bool) {
- q, _, ok := Quo32(a, b)
- return q, ok
-}
-
-// Div32p is the unchecked panicking version of Div32
-func Div32p(a, b int32) int32 {
- r, ok := Div32(a, b)
- if !ok {
- panic("division failure")
- }
- return r
-}
-
-// Quo32 performs + operation on two int32 operands
-// returning a quotient, a remainder and status
-func Quo32(a, b int32) (int32, int32, bool) {
- if b == 0 {
- return 0, 0, false
- } else if b == -1 && a == int32(math.MinInt32) {
- return 0, 0, false
- }
- c := a / b
- return c, a % b, true
-}
-
-// Add64 performs + operation on two int64 operands
-// returning a result and status
-func Add64(a, b int64) (int64, bool) {
- c := a + b
- if (c > a) == (b > 0) {
- return c, true
- }
- return c, false
-}
-
-// Add64p is the unchecked panicking version of Add64
-func Add64p(a, b int64) int64 {
- r, ok := Add64(a, b)
- if !ok {
- panic("addition overflow")
- }
- return r
-}
-
-// Sub64 performs - operation on two int64 operands
-// returning a result and status
-func Sub64(a, b int64) (int64, bool) {
- c := a - b
- if (c < a) == (b > 0) {
- return c, true
- }
- return c, false
-}
-
-// Sub64p is the unchecked panicking version of Sub64
-func Sub64p(a, b int64) int64 {
- r, ok := Sub64(a, b)
- if !ok {
- panic("subtraction overflow")
- }
- return r
-}
-
-// Mul64 performs * operation on two int64 operands
-// returning a result and status
-func Mul64(a, b int64) (int64, bool) {
- if a == 0 || b == 0 {
- return 0, true
- }
- c := a * b
- if (c < 0) == ((a < 0) != (b < 0)) {
- if c/b == a {
- return c, true
- }
- }
- return c, false
-}
-
-// Mul64p is the unchecked panicking version of Mul64
-func Mul64p(a, b int64) int64 {
- r, ok := Mul64(a, b)
- if !ok {
- panic("multiplication overflow")
- }
- return r
-}
-
-// Div64 performs / operation on two int64 operands
-// returning a result and status
-func Div64(a, b int64) (int64, bool) {
- q, _, ok := Quo64(a, b)
- return q, ok
-}
-
-// Div64p is the unchecked panicking version of Div64
-func Div64p(a, b int64) int64 {
- r, ok := Div64(a, b)
- if !ok {
- panic("division failure")
- }
- return r
-}
-
-// Quo64 performs + operation on two int64 operands
-// returning a quotient, a remainder and status
-func Quo64(a, b int64) (int64, int64, bool) {
- if b == 0 {
- return 0, 0, false
- } else if b == -1 && a == math.MinInt64 {
- return 0, 0, false
- }
- c := a / b
- return c, a % b, true
-}
diff --git a/gnovm/stdlibs/math/overflow/overflow_test.gno b/gnovm/stdlibs/math/overflow/overflow_test.gno
deleted file mode 100644
index b7881aec480..00000000000
--- a/gnovm/stdlibs/math/overflow/overflow_test.gno
+++ /dev/null
@@ -1,200 +0,0 @@
-package overflow
-
-import (
- "math"
- "testing"
-)
-
-// sample all possibilities of 8 bit numbers
-// by checking against 64 bit numbers
-
-func TestAlgorithms(t *testing.T) {
- errors := 0
-
- for a64 := int64(math.MinInt8); a64 <= int64(math.MaxInt8); a64++ {
- for b64 := int64(math.MinInt8); b64 <= int64(math.MaxInt8) && errors < 10; b64++ {
-
- a8 := int8(a64)
- b8 := int8(b64)
-
- if int64(a8) != a64 || int64(b8) != b64 {
- t.Fatal("LOGIC FAILURE IN TEST")
- }
-
- // ADDITION
- {
- r64 := a64 + b64
-
- // now the verification
- result, ok := Add8(a8, b8)
- if ok && int64(result) != r64 {
- t.Errorf("failed to fail on %v + %v = %v instead of %v\n",
- a8, b8, result, r64)
- errors++
- }
- if !ok && int64(result) == r64 {
- t.Fail()
- errors++
- }
- }
-
- // SUBTRACTION
- {
- r64 := a64 - b64
-
- // now the verification
- result, ok := Sub8(a8, b8)
- if ok && int64(result) != r64 {
- t.Errorf("failed to fail on %v - %v = %v instead of %v\n",
- a8, b8, result, r64)
- }
- if !ok && int64(result) == r64 {
- t.Fail()
- errors++
- }
- }
-
- // MULTIPLICATION
- {
- r64 := a64 * b64
-
- // now the verification
- result, ok := Mul8(a8, b8)
- if ok && int64(result) != r64 {
- t.Errorf("failed to fail on %v * %v = %v instead of %v\n",
- a8, b8, result, r64)
- errors++
- }
- if !ok && int64(result) == r64 {
- t.Fail()
- errors++
- }
- }
-
- // DIVISION
- if b8 != 0 {
- r64 := a64 / b64
- rem64 := a64 % b64
-
- // now the verification
- result, rem, ok := Quo8(a8, b8)
- if ok && int64(result) != r64 {
- t.Errorf("failed to fail on %v / %v = %v instead of %v\n",
- a8, b8, result, r64)
- errors++
- }
- if ok && int64(rem) != rem64 {
- t.Errorf("failed to fail on %v %% %v = %v instead of %v\n",
- a8, b8, rem, rem64)
- errors++
- }
- }
- }
- }
-}
-
-func TestQuotient(t *testing.T) {
- q, r, ok := Quo(100, 3)
- if r != 1 || q != 33 || !ok {
- t.Errorf("expected 100/3 => 33, r=1")
- }
- if _, _, ok = Quo(1, 0); ok {
- t.Error("unexpected lack of failure")
- }
-}
-
-func TestLong(t *testing.T) {
- if testing.Short() {
- t.Skip()
- }
-
- ctr := int64(0)
-
- for a64 := int64(math.MinInt16); a64 <= int64(math.MaxInt16); a64++ {
- for b64 := int64(math.MinInt16); b64 <= int64(math.MaxInt16); b64++ {
- a16 := int16(a64)
- b16 := int16(b64)
- if int64(a16) != a64 || int64(b16) != b64 {
- panic("LOGIC FAILURE IN TEST")
- }
- ctr++
-
- // ADDITION
- {
- r64 := a64 + b64
-
- // now the verification
- result, ok := Add16(a16, b16)
- if int64(math.MinInt16) <= r64 && r64 <= int64(math.MaxInt16) {
- if !ok || int64(result) != r64 {
- println("add", a16, b16, result, r64)
- panic("incorrect result for non-overflow")
- }
- } else {
- if ok {
- println("add", a16, b16, result, r64)
- panic("incorrect ok result")
- }
- }
- }
-
- // SUBTRACTION
- {
- r64 := a64 - b64
-
- // now the verification
- result, ok := Sub16(a16, b16)
- if int64(math.MinInt16) <= r64 && r64 <= int64(math.MaxInt16) {
- if !ok || int64(result) != r64 {
- println("sub", a16, b16, result, r64)
- panic("incorrect result for non-overflow")
- }
- } else {
- if ok {
- println("sub", a16, b16, result, r64)
- panic("incorrect ok result")
- }
- }
- }
-
- // MULTIPLICATION
- {
- r64 := a64 * b64
-
- // now the verification
- result, ok := Mul16(a16, b16)
- if int64(math.MinInt16) <= r64 && r64 <= int64(math.MaxInt16) {
- if !ok || int64(result) != r64 {
- println("mul", a16, b16, result, r64)
- panic("incorrect result for non-overflow")
- }
- } else {
- if ok {
- println("mul", a16, b16, result, r64)
- panic("incorrect ok result")
- }
- }
- }
-
- // DIVISION
- if b16 != 0 {
- r64 := a64 / b64
-
- // now the verification
- result, _, ok := Quo16(a16, b16)
- if int64(math.MinInt16) <= r64 && r64 <= int64(math.MaxInt16) {
- if !ok || int64(result) != r64 {
- println("quo", a16, b16, result, r64)
- panic("incorrect result for non-overflow")
- }
- } else {
- if ok {
- println("quo", a16, b16, result, r64)
- panic("incorrect ok result")
- }
- }
- }
- }
- }
- println("done", ctr)
-}
diff --git a/gnovm/stdlibs/std/coins.gno b/gnovm/stdlibs/std/coins.gno
index 47e88e238d2..679674e443e 100644
--- a/gnovm/stdlibs/std/coins.gno
+++ b/gnovm/stdlibs/std/coins.gno
@@ -1,9 +1,6 @@
package std
-import (
- "math/overflow"
- "strconv"
-)
+import "strconv"
// NOTE: this is selectively copied over from tm2/pkgs/std/coin.go
@@ -56,13 +53,7 @@ func (c Coin) IsEqual(other Coin) bool {
// An invalid result panics.
func (c Coin) Add(other Coin) Coin {
mustMatchDenominations(c.Denom, other.Denom)
-
- sum, ok := overflow.Add64(c.Amount, other.Amount)
- if !ok {
- panic("coin add overflow/underflow: " + strconv.Itoa(int(c.Amount)) + " +/- " + strconv.Itoa(int(other.Amount)))
- }
-
- c.Amount = sum
+ c.Amount += other.Amount
return c
}
@@ -72,13 +63,7 @@ func (c Coin) Add(other Coin) Coin {
// An invalid result panics.
func (c Coin) Sub(other Coin) Coin {
mustMatchDenominations(c.Denom, other.Denom)
-
- dff, ok := overflow.Sub64(c.Amount, other.Amount)
- if !ok {
- panic("coin sub overflow/underflow: " + strconv.Itoa(int(c.Amount)) + " +/- " + strconv.Itoa(int(other.Amount)))
- }
- c.Amount = dff
-
+ c.Amount -= other.Amount
return c
}
@@ -113,10 +98,7 @@ func NewCoins(coins ...Coin) Coins {
for _, coin := range coins {
if currentAmount, exists := coinMap[coin.Denom]; exists {
- var ok bool
- if coinMap[coin.Denom], ok = overflow.Add64(currentAmount, coin.Amount); !ok {
- panic("coin sub overflow/underflow: " + strconv.Itoa(int(currentAmount)) + " +/- " + strconv.Itoa(int(coin.Amount)))
- }
+ coinMap[coin.Denom] = currentAmount + coin.Amount
} else {
coinMap[coin.Denom] = coin.Amount
}
diff --git a/gnovm/tests/files/overflow0.gno b/gnovm/tests/files/overflow0.gno
new file mode 100644
index 00000000000..1313f064322
--- /dev/null
+++ b/gnovm/tests/files/overflow0.gno
@@ -0,0 +1,10 @@
+package main
+
+func main() {
+ var a, b, c int8 = -1<<7, -1, 0
+ c = a / b // overflow: -128 instead of 128
+ println(c)
+}
+
+// Error:
+// division by zero or overflow
diff --git a/gnovm/tests/files/overflow1.gno b/gnovm/tests/files/overflow1.gno
new file mode 100644
index 00000000000..a416e9a3498
--- /dev/null
+++ b/gnovm/tests/files/overflow1.gno
@@ -0,0 +1,10 @@
+package main
+
+func main() {
+ var a, b, c int16 = -1<<15, -1, 0
+ c = a / b // overflow: -32768 instead of 32768
+ println(c)
+}
+
+// Error:
+// division by zero or overflow
diff --git a/gnovm/tests/files/overflow2.gno b/gnovm/tests/files/overflow2.gno
new file mode 100644
index 00000000000..353729bcdf2
--- /dev/null
+++ b/gnovm/tests/files/overflow2.gno
@@ -0,0 +1,10 @@
+package main
+
+func main() {
+ var a, b, c int32 = -1<<31, -1, 0
+ c = a / b // overflow: -2147483648 instead of 2147483648
+ println(c)
+}
+
+// Error:
+// division by zero or overflow
diff --git a/gnovm/tests/files/overflow3.gno b/gnovm/tests/files/overflow3.gno
new file mode 100644
index 00000000000..a09c59dfb03
--- /dev/null
+++ b/gnovm/tests/files/overflow3.gno
@@ -0,0 +1,10 @@
+package main
+
+func main() {
+ var a, b, c int64 = -1<<63, -1, 0
+ c = a / b // overflow: -9223372036854775808 instead of 9223372036854775808
+ println(c)
+}
+
+// Error:
+// division by zero or overflow
diff --git a/gnovm/tests/files/overflow4.gno b/gnovm/tests/files/overflow4.gno
new file mode 100644
index 00000000000..26b05567b07
--- /dev/null
+++ b/gnovm/tests/files/overflow4.gno
@@ -0,0 +1,10 @@
+package main
+
+func main() {
+ var a, b, c int = -1<<63, -1, 0
+ c = a / b // overflow: -9223372036854775808 instead of 9223372036854775808
+ println(c)
+}
+
+// Error:
+// division by zero or overflow
diff --git a/gnovm/tests/files/overflow5.gno b/gnovm/tests/files/overflow5.gno
new file mode 100644
index 00000000000..ef7f976eb24
--- /dev/null
+++ b/gnovm/tests/files/overflow5.gno
@@ -0,0 +1,10 @@
+package main
+
+func main() {
+ var a, b, c int = -5, 7, 0
+ c = a % b // 0 quotient triggers a false negative in gnolang/overflow
+ println(c)
+}
+
+// Output:
+// -5
diff --git a/gnovm/tests/files/recover14.gno b/gnovm/tests/files/recover14.gno
index 30a34ab291a..3c96404fcbe 100644
--- a/gnovm/tests/files/recover14.gno
+++ b/gnovm/tests/files/recover14.gno
@@ -12,4 +12,4 @@ func main() {
}
// Output:
-// recover: division by zero
+// recover: division by zero or overflow
diff --git a/go.mod b/go.mod
index ed7c3b75528..cd038e2ae65 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
github.com/davecgh/go-spew v1.1.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
github.com/fortytw2/leaktest v1.3.0
+ github.com/google/go-cmp v0.6.0
github.com/google/gofuzz v1.2.0
github.com/gorilla/websocket v1.5.3
github.com/libp2p/go-buffer-pool v0.1.0
@@ -27,6 +28,7 @@ require (
github.com/stretchr/testify v1.9.0
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
github.com/yuin/goldmark v1.7.2
+ github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.3.11
go.opentelemetry.io/otel v1.29.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0
diff --git a/go.sum b/go.sum
index a4ccfbbdd66..9c4d20dbad6 100644
--- a/go.sum
+++ b/go.sum
@@ -3,8 +3,10 @@ dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
+github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
@@ -51,6 +53,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
+github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
+github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
@@ -144,8 +148,11 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
+github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.2 h1:NjGd7lO7zrUn/A7eKwn5PEOt4ONYGqpxSEeZuduvgxc=
github.com/yuin/goldmark v1.7.2/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
+github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U=
github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM=
github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw=
diff --git a/misc/autocounterd/go.sum b/misc/autocounterd/go.sum
index e4051e3a5a4..bd88dd5d08c 100644
--- a/misc/autocounterd/go.sum
+++ b/misc/autocounterd/go.sum
@@ -157,10 +157,6 @@ go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeX
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
-go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
diff --git a/misc/docs-linter/jsx.go b/misc/docs-linter/jsx.go
index d0307680a0c..eb041a78386 100644
--- a/misc/docs-linter/jsx.go
+++ b/misc/docs-linter/jsx.go
@@ -50,7 +50,7 @@ func lintJSX(filepathToJSX map[string][]string) (string, error) {
found = true
}
- output.WriteString(fmt.Sprintf(">>> %s (found in file: %s)\n", tag, filePath))
+ fmt.Fprintf(&output, ">>> %s (found in file: %s)\n", tag, filePath)
}
}
diff --git a/misc/docs-linter/links.go b/misc/docs-linter/links.go
index 744917d8dfb..e34d35d9f58 100644
--- a/misc/docs-linter/links.go
+++ b/misc/docs-linter/links.go
@@ -80,7 +80,7 @@ func lintLocalLinks(filepathToLinks map[string][]string, docsPath string) (strin
found = true
}
- output.WriteString(fmt.Sprintf(">>> %s (found in file: %s)\n", link, filePath))
+ fmt.Fprintf(&output, ">>> %s (found in file: %s)\n", link, filePath)
}
}
}
diff --git a/misc/docs-linter/main.go b/misc/docs-linter/main.go
index 97d80316108..5d7cdf37982 100644
--- a/misc/docs-linter/main.go
+++ b/misc/docs-linter/main.go
@@ -61,8 +61,8 @@ func execLint(cfg *cfg, ctx context.Context) (string, error) {
}
// Main buffer to write to the end user after linting
- var output bytes.Buffer
- output.WriteString(fmt.Sprintf("Linting %s...\n", absPath))
+ var output bytes.Buffer
+ fmt.Fprintf(&output, "Linting %s...\n", absPath)
// Find docs files to lint
mdFiles, err := findFilePaths(cfg.docsPath)
diff --git a/misc/docs-linter/urls.go b/misc/docs-linter/urls.go
index 093e624d81e..098d0a05524 100644
--- a/misc/docs-linter/urls.go
+++ b/misc/docs-linter/urls.go
@@ -66,7 +66,7 @@ func lintURLs(filepathToURLs map[string][]string, ctx context.Context) (string,
found = true
}
- output.WriteString(fmt.Sprintf(">>> %s (found in file: %s)\n", url, filePath))
+ fmt.Fprintf(&output, ">>> %s (found in file: %s)\n", url, filePath)
lock.Unlock()
}
diff --git a/misc/genstd/util.go b/misc/genstd/util.go
index 025fe4b673e..13e90836f36 100644
--- a/misc/genstd/util.go
+++ b/misc/genstd/util.go
@@ -70,7 +70,8 @@ func findDirs() (gitRoot string, relPath string, err error) {
}
p := wd
for {
- if s, e := os.Stat(filepath.Join(p, ".git")); e == nil && s.IsDir() {
+ // .git is normally a directory, or a file in case of a git worktree.
+ if _, e := os.Stat(filepath.Join(p, ".git")); e == nil {
// make relPath relative to the git root
rp := strings.TrimPrefix(wd, p+string(filepath.Separator))
// normalize separator to /
diff --git a/tm2/pkg/bft/config/config.go b/tm2/pkg/bft/config/config.go
index b745c815886..1a01686f4bd 100644
--- a/tm2/pkg/bft/config/config.go
+++ b/tm2/pkg/bft/config/config.go
@@ -18,6 +18,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/errors"
osm "github.com/gnolang/gno/tm2/pkg/os"
p2p "github.com/gnolang/gno/tm2/pkg/p2p/config"
+ sdk "github.com/gnolang/gno/tm2/pkg/sdk/config"
telemetry "github.com/gnolang/gno/tm2/pkg/telemetry/config"
)
@@ -55,6 +56,7 @@ type Config struct {
Consensus *cns.ConsensusConfig `json:"consensus" toml:"consensus" comment:"##### consensus configuration options #####"`
TxEventStore *eventstore.Config `json:"tx_event_store" toml:"tx_event_store" comment:"##### event store #####"`
Telemetry *telemetry.Config `json:"telemetry" toml:"telemetry" comment:"##### node telemetry #####"`
+ Application *sdk.AppConfig `json:"application" toml:"application" comment:"##### app settings #####"`
}
// DefaultConfig returns a default configuration for a Tendermint node
@@ -67,6 +69,7 @@ func DefaultConfig() *Config {
Consensus: cns.DefaultConsensusConfig(),
TxEventStore: eventstore.DefaultEventStoreConfig(),
Telemetry: telemetry.DefaultTelemetryConfig(),
+ Application: sdk.DefaultAppConfig(),
}
}
@@ -183,6 +186,7 @@ func TestConfig() *Config {
Consensus: cns.TestConsensusConfig(),
TxEventStore: eventstore.DefaultEventStoreConfig(),
Telemetry: telemetry.DefaultTelemetryConfig(),
+ Application: sdk.DefaultAppConfig(),
}
}
@@ -238,6 +242,9 @@ func (cfg *Config) ValidateBasic() error {
if err := cfg.Consensus.ValidateBasic(); err != nil {
return errors.Wrap(err, "Error in [consensus] section")
}
+ if err := cfg.Application.ValidateBasic(); err != nil {
+ return errors.Wrap(err, "Error in [application] section")
+ }
return nil
}
diff --git a/tm2/pkg/bft/rpc/lib/server/handlers.go b/tm2/pkg/bft/rpc/lib/server/handlers.go
index 88ee26da4a9..9e10596a975 100644
--- a/tm2/pkg/bft/rpc/lib/server/handlers.go
+++ b/tm2/pkg/bft/rpc/lib/server/handlers.go
@@ -939,7 +939,7 @@ func writeListOfEndpoints(w http.ResponseWriter, r *http.Request, funcMap map[st
for _, name := range noArgNames {
link := fmt.Sprintf("//%s/%s", r.Host, name)
- buf.WriteString(fmt.Sprintf("%s", link, link))
+ fmt.Fprintf(buf, "%s", link, link)
}
buf.WriteString("
Endpoints that require arguments:
")
@@ -952,7 +952,7 @@ func writeListOfEndpoints(w http.ResponseWriter, r *http.Request, funcMap map[st
link += "&"
}
}
- buf.WriteString(fmt.Sprintf("%s", link, link))
+ fmt.Fprintf(buf, "%s", link, link)
}
buf.WriteString("