diff --git a/cmd/dmsg-discovery/commands/dmsg-discovery.go b/cmd/dmsg-discovery/commands/dmsg-discovery.go index 06409dab2..c26023e56 100644 --- a/cmd/dmsg-discovery/commands/dmsg-discovery.go +++ b/cmd/dmsg-discovery/commands/dmsg-discovery.go @@ -8,7 +8,6 @@ import ( "log" "net" "net/http" - "net/http/pprof" "os" "strings" "time" @@ -22,6 +21,7 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/metricsutil" "github.com/spf13/cobra" + dmsgcmdutil "github.com/skycoin/dmsg/pkg/cmdutil" "github.com/skycoin/dmsg/pkg/direct" "github.com/skycoin/dmsg/pkg/disc" "github.com/skycoin/dmsg/pkg/disc/metrics" @@ -49,6 +49,7 @@ var ( authPassphrase string officialServers string dmsgServerType string + pprofMode string pprofAddr string ) @@ -56,7 +57,8 @@ func init() { sf.Init(RootCmd, "dmsg_disc", "") RootCmd.Flags().StringVarP(&addr, "addr", "a", ":9090", "address to bind to\n\r") - RootCmd.Flags().StringVar(&pprofAddr, "pprof", "", "address to bind pprof debug server (e.g. localhost:6060)") + RootCmd.Flags().StringVar(&pprofMode, "pprofmode", "", "[ cpu | mem | mutex | block | trace | http ]") + RootCmd.Flags().StringVar(&pprofAddr, "pprofaddr", "localhost:6060", "pprof http port") RootCmd.Flags().StringVar(&authPassphrase, "auth", "", "auth passphrase as simple auth for official dmsg servers registration") RootCmd.Flags().StringVar(&officialServers, "official-servers", "", "list of official dmsg servers keys separated by comma") RootCmd.Flags().StringVar(&redisURL, "redis", store.DefaultURL, "connections string for a redis store\n\r") @@ -117,38 +119,8 @@ Example: log.WithError(err).Warn("No SecKey found. Skipping serving on dmsghttp.") } - if pprofAddr != "" { - pprofMux := http.NewServeMux() - - // Register the index (which links to everything else) - pprofMux.HandleFunc("/debug/pprof/", pprof.Index) - pprofMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - pprofMux.HandleFunc("/debug/pprof/profile", pprof.Profile) - pprofMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - pprofMux.HandleFunc("/debug/pprof/trace", pprof.Trace) - - // Register profile handlers using pprof.Handler - for _, profile := range []string{"heap", "goroutine", "threadcreate", "block", "mutex", "allocs"} { - pprofMux.Handle("/debug/pprof/"+profile, pprof.Handler(profile)) - } - - go func() { - log.Infof("Starting pprof server on %s", pprofAddr) - server := &http.Server{ - Addr: pprofAddr, - Handler: pprofMux, - ReadHeaderTimeout: 10 * time.Second, - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - IdleTimeout: 60 * time.Second, - } - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Errorf("pprof server failed: %v", err) - } - }() - - time.Sleep(100 * time.Millisecond) - } + stopPProf := dmsgcmdutil.InitPProf(log, pprofMode, pprofAddr) + defer stopPProf() metricsutil.ServeHTTPMetrics(log, sf.MetricsAddr) diff --git a/cmd/dmsg-server/commands/start/root.go b/cmd/dmsg-server/commands/start/root.go index f48f49bcf..605ddd80b 100644 --- a/cmd/dmsg-server/commands/start/root.go +++ b/cmd/dmsg-server/commands/start/root.go @@ -7,11 +7,9 @@ import ( "io" "log" "net/http" - "net/http/pprof" "net/url" "os" "strconv" - "time" chi "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -21,6 +19,7 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/metricsutil" "github.com/spf13/cobra" + dmsgcmdutil "github.com/skycoin/dmsg/pkg/cmdutil" "github.com/skycoin/dmsg/pkg/disc" dmsg "github.com/skycoin/dmsg/pkg/dmsg" "github.com/skycoin/dmsg/pkg/dmsg/metrics" @@ -31,12 +30,14 @@ import ( var ( sf cmdutil.ServiceFlags authPassphrase string + pprofMode string pprofAddr string ) func init() { sf.Init(RootCmd, "dmsg_srv", dmsgserver.DefaultConfigPath) - RootCmd.Flags().StringVar(&pprofAddr, "pprof", "", "address to bind pprof debug server (e.g. localhost:6060)\033[0m") + RootCmd.Flags().StringVar(&pprofMode, "pprofmode", "", "[ cpu | mem | mutex | block | trace | http ]") + RootCmd.Flags().StringVar(&pprofAddr, "pprofaddr", "localhost:6060", "pprof http port\033[0m") RootCmd.Flags().StringVar(&authPassphrase, "auth", "", "auth passphrase as simple auth for official dmsg servers registration") } @@ -63,38 +64,8 @@ var RootCmd = &cobra.Command{ } logging.SetLevel(logLvl) - if pprofAddr != "" { - pprofMux := http.NewServeMux() - - // Register the index (which links to everything else) - pprofMux.HandleFunc("/debug/pprof/", pprof.Index) - pprofMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - pprofMux.HandleFunc("/debug/pprof/profile", pprof.Profile) - pprofMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - pprofMux.HandleFunc("/debug/pprof/trace", pprof.Trace) - - // Register profile handlers using pprof.Handler - for _, profile := range []string{"heap", "goroutine", "threadcreate", "block", "mutex", "allocs"} { - pprofMux.Handle("/debug/pprof/"+profile, pprof.Handler(profile)) - } - - go func() { - log.Infof("Starting pprof server on %s", pprofAddr) - server := &http.Server{ - Addr: pprofAddr, - Handler: pprofMux, - ReadHeaderTimeout: 10 * time.Second, - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - IdleTimeout: 60 * time.Second, - } - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Errorf("pprof server failed: %v", err) - } - }() - - time.Sleep(100 * time.Millisecond) - } + stopPProf := dmsgcmdutil.InitPProf(log, pprofMode, pprofAddr) + defer stopPProf() if conf.HTTPAddress == "" { u, err := url.Parse(conf.LocalAddress) diff --git a/cmd/dmsg-socks5/commands/dmsg-socks5.go b/cmd/dmsg-socks5/commands/dmsg-socks5.go index 9b86e10a9..0ced2af61 100644 --- a/cmd/dmsg-socks5/commands/dmsg-socks5.go +++ b/cmd/dmsg-socks5/commands/dmsg-socks5.go @@ -17,6 +17,7 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "github.com/spf13/cobra" + dmsgcmdutil "github.com/skycoin/dmsg/pkg/cmdutil" dmsg "github.com/skycoin/dmsg/pkg/dmsg" "github.com/skycoin/dmsg/pkg/dmsgclient" ) @@ -34,6 +35,8 @@ var ( dlog *logging.Logger dmsgHTTPPath string err error + pprofMode string + pprofAddr string ) // Execute executes root CLI command. @@ -55,6 +58,8 @@ func init() { sk.Set(os.Getenv("DMSGSK")) //nolint } serveCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\033[0m\n\r") + serveCmd.Flags().StringVar(&pprofMode, "pprofmode", "", "[ cpu | mem | mutex | block | trace | http ]") + serveCmd.Flags().StringVar(&pprofAddr, "pprofaddr", "localhost:6060", "pprof http port") proxyCmd.Flags().IntVarP(&proxyPort, "port", "p", 1081, "TCP port to serve SOCKS5 proxy locally\033[0m\n\r") proxyCmd.Flags().Uint16VarP(&dmsgPort, "dport", "q", 1081, "dmsg port to connect to socks5 server\033[0m\n\r") @@ -66,6 +71,8 @@ func init() { sk.Set(os.Getenv("DMSGSK")) //nolint } proxyCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\033[0m\n\r") + proxyCmd.Flags().StringVar(&pprofMode, "pprofmode", "", "[ cpu | mem | mutex | block | trace | http ]") + proxyCmd.Flags().StringVar(&pprofAddr, "pprofaddr", "localhost:6060", "pprof http port") } @@ -93,6 +100,8 @@ var serveCmd = &cobra.Command{ DisableFlagsInUseLine: true, Run: func(_ *cobra.Command, _ []string) { dlog = logging.MustGetLogger("dmsg-proxy") + stopPProf := dmsgcmdutil.InitPProf(dlog, pprofMode, pprofAddr) + defer stopPProf() if dmsgHTTPPath != "" { dmsg.DmsghttpJSON, err = os.ReadFile(dmsgHTTPPath) //nolint @@ -204,6 +213,8 @@ var proxyCmd = &cobra.Command{ DisableFlagsInUseLine: true, Run: func(_ *cobra.Command, _ []string) { dlog = logging.MustGetLogger("dmsg-proxy-client") + stopPProf := dmsgcmdutil.InitPProf(dlog, pprofMode, pprofAddr) + defer stopPProf() if dmsgHTTPPath != "" { dmsg.DmsghttpJSON, err = os.ReadFile(dmsgHTTPPath) //nolint diff --git a/cmd/dmsghttp/commands/dmsghttp.go b/cmd/dmsghttp/commands/dmsghttp.go index a2ff3c05f..01d0c47a3 100644 --- a/cmd/dmsghttp/commands/dmsghttp.go +++ b/cmd/dmsghttp/commands/dmsghttp.go @@ -19,6 +19,7 @@ import ( "github.com/spf13/cobra" "golang.org/x/net/proxy" + dmsgcmdutil "github.com/skycoin/dmsg/pkg/cmdutil" dmsg "github.com/skycoin/dmsg/pkg/dmsg" "github.com/skycoin/dmsg/pkg/dmsgclient" ) @@ -34,6 +35,8 @@ var ( wl []string wlkeys []cipher.PubKey err error + pprofMode string + pprofAddr string ) func init() { @@ -48,6 +51,8 @@ func init() { sk.Set(os.Getenv("DMSGHTTP_SK")) //nolint } RootCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\033[0m\n\r") + RootCmd.Flags().StringVar(&pprofMode, "pprofmode", "", "[ cpu | mem | mutex | block | trace | http ]") + RootCmd.Flags().StringVar(&pprofAddr, "pprofaddr", "localhost:6060", "pprof http port") } @@ -69,6 +74,9 @@ var RootCmd = &cobra.Command{ } func server() { + stopPProf := dmsgcmdutil.InitPProf(dlog, pprofMode, pprofAddr) + defer stopPProf() + wg := new(sync.WaitGroup) wg.Add(1) diff --git a/cmd/dmsgpty-host/commands/root.go b/cmd/dmsgpty-host/commands/root.go index cc2c8a3f8..017223f34 100644 --- a/cmd/dmsgpty-host/commands/root.go +++ b/cmd/dmsgpty-host/commands/root.go @@ -21,6 +21,7 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "github.com/spf13/cobra" + dmsgcmdutil "github.com/skycoin/dmsg/pkg/cmdutil" "github.com/skycoin/dmsg/pkg/disc" dmsg "github.com/skycoin/dmsg/pkg/dmsg" "github.com/skycoin/dmsg/pkg/dmsgclient" @@ -51,6 +52,8 @@ var ( // root command flags confStdin = false confPath = "./config.json" + pprofMode string + pprofAddr string ) // init prepares flags. @@ -67,6 +70,8 @@ func init() { RootCmd.Flags().StringVar(&envPrefix, "envprefix", envPrefix, "env prefix") RootCmd.Flags().BoolVar(&confStdin, "confstdin", confStdin, "config will be read from stdin if set") RootCmd.Flags().StringVarP(&confPath, "confpath", "c", confPath, "config path") + RootCmd.Flags().StringVar(&pprofMode, "pprofmode", "", "[ cpu | mem | mutex | block | trace | http ]") + RootCmd.Flags().StringVar(&pprofAddr, "pprofaddr", "localhost:6060", "pprof http port") } @@ -90,6 +95,8 @@ var RootCmd = &cobra.Command{ log.Printf("Failed to output build info: %v", err) } log := logging.MustGetLogger("dmsgpty-host") + stopPProf := dmsgcmdutil.InitPProf(log, pprofMode, pprofAddr) + defer stopPProf() ctx, cancel := cmdutil.SignalContext(context.Background(), log) defer cancel() diff --git a/cmd/dmsgweb/commands/dmsgweb.go b/cmd/dmsgweb/commands/dmsgweb.go index 8d80abac7..c505065c7 100644 --- a/cmd/dmsgweb/commands/dmsgweb.go +++ b/cmd/dmsgweb/commands/dmsgweb.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" "golang.org/x/net/proxy" + dmsgcmdutil "github.com/skycoin/dmsg/pkg/cmdutil" dmsg "github.com/skycoin/dmsg/pkg/dmsg" "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsghttp" @@ -74,6 +75,8 @@ func init() { RootCmd.Flags().StringVarP(&logLvl, "loglvl", "l", "debug", "[ debug | warn | error | fatal | panic | trace | info ]\033[0m\n\r") RootCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\n\r") RootCmd.Flags().BoolVarP(&isEnvs, "envs", "E", false, "show example .conf file\033[0m\n\r") + RootCmd.Flags().StringVar(&pprofMode, "pprofmode", "", "[ cpu | mem | mutex | block | trace | http ]") + RootCmd.Flags().StringVar(&pprofAddr, "pprofaddr", "localhost:6060", "pprof http port") } @@ -160,6 +163,9 @@ dmsgweb conf file detected: ` + dwcfg } }, Run: func(_ *cobra.Command, _ []string) { + stopPProf := dmsgcmdutil.InitPProf(dlog, pprofMode, pprofAddr) + defer stopPProf() + ctx, cancel := cmdutil.SignalContext(context.Background(), dlog) defer cancel() diff --git a/cmd/dmsgweb/commands/dmsgwebsrv.go b/cmd/dmsgweb/commands/dmsgwebsrv.go index 18d94c5ca..de4dd7eb4 100644 --- a/cmd/dmsgweb/commands/dmsgwebsrv.go +++ b/cmd/dmsgweb/commands/dmsgwebsrv.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/cobra" "golang.org/x/net/proxy" + dmsgcmdutil "github.com/skycoin/dmsg/pkg/cmdutil" "github.com/skycoin/dmsg/pkg/dmsgclient" ) @@ -60,6 +61,8 @@ func init() { srvCmd.Flags().StringVarP(&logLvl, "loglvl", "l", "debug", "[ debug | warn | error | fatal | panic | trace | info ]\033[0m\n\r") srvCmd.Flags().BoolVarP(&isEnvs, "envs", "E", false, "show example .conf file") srvCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\033[0m\n\r") + srvCmd.Flags().StringVar(&pprofMode, "pprofmode", "", "[ cpu | mem | mutex | block | trace | http ]") + srvCmd.Flags().StringVar(&pprofAddr, "pprofaddr", "localhost:6060", "pprof http port") srvCmd.CompletionOptions.DisableDefaultCmd = true } @@ -114,6 +117,9 @@ var srvCmd = &cobra.Command{ } func server() { + stopPProf := dmsgcmdutil.InitPProf(dlog, pprofMode, pprofAddr) + defer stopPProf() + ctx, cancel := cmdutil.SignalContext(context.Background(), dlog) defer cancel() diff --git a/cmd/dmsgweb/commands/root.go b/cmd/dmsgweb/commands/root.go index 3d1d24da4..7e7c14ae6 100644 --- a/cmd/dmsgweb/commands/root.go +++ b/cmd/dmsgweb/commands/root.go @@ -52,6 +52,8 @@ var ( rawTCP []bool httpClient *http.Client //nolint unused dialer proxy.Dialer = proxy.Direct + pprofMode string + pprofAddr string ) // Execute executes root CLI command. diff --git a/go.mod b/go.mod index 9ff1dbe79..b2a6086be 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/pires/go-proxyproto v0.11.0 github.com/sirupsen/logrus v1.9.4 github.com/skycoin/noise v0.0.0-20180327030543-2492fe189ae6 - github.com/skycoin/skycoin v0.28.5-alpha1.0.20260323015226-90b668188f85 + github.com/skycoin/skycoin v0.28.6-0.20260325014814-f48988877c68 github.com/skycoin/skywire v1.3.37 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 @@ -77,7 +77,7 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/quic-go/qpack v0.6.0 // indirect diff --git a/go.sum b/go.sum index 507835333..88256239f 100644 --- a/go.sum +++ b/go.sum @@ -150,8 +150,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -170,8 +170,8 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skycoin/noise v0.0.0-20180327030543-2492fe189ae6 h1:1Nc5EBY6pjfw1kwW0duwyG+7WliWz5u9kgk1h5MnLuA= github.com/skycoin/noise v0.0.0-20180327030543-2492fe189ae6/go.mod h1:UXghlricA7J3aRD/k7p/zBObQfmBawwCxIVPVjz2Q3o= -github.com/skycoin/skycoin v0.28.5-alpha1.0.20260323015226-90b668188f85 h1:IiU8PjIzg/BlTpSXXgq1pVCEbKnDfGMnUAqgwwjNt4s= -github.com/skycoin/skycoin v0.28.5-alpha1.0.20260323015226-90b668188f85/go.mod h1:tgVxjBBV4/OxVBDrcpsVK0q/awGxqBjwTUPDBMh9ZcA= +github.com/skycoin/skycoin v0.28.6-0.20260325014814-f48988877c68 h1:M+VmlCByq6jRbxio6GOn4h+5jP+nmumiu1xMZZg3MZI= +github.com/skycoin/skycoin v0.28.6-0.20260325014814-f48988877c68/go.mod h1:tgVxjBBV4/OxVBDrcpsVK0q/awGxqBjwTUPDBMh9ZcA= github.com/skycoin/skywire v1.3.37 h1:LIgOrj6PqdH6RAOWsD8TSI/vTyp7kUYE2Ale6pkvjJw= github.com/skycoin/skywire v1.3.37/go.mod h1:k3TA1edIXR96Jtec5XYVy7EGHQlZL524pCPYRTzBBok= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= diff --git a/pkg/cmdutil/pprof.go b/pkg/cmdutil/pprof.go new file mode 100644 index 000000000..99095e0e8 --- /dev/null +++ b/pkg/cmdutil/pprof.go @@ -0,0 +1,151 @@ +// Package cmdutil provides shared command utilities for dmsg services. +package cmdutil + +import ( + "net/http" + "net/http/pprof" + "os" + "runtime" + rpprof "runtime/pprof" + "time" + + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" +) + +// InitPProf starts profiling based on the given mode and address. +// Supported modes: http, cpu, mem, mutex, block, trace. +// If mode is empty, this is a no-op. +// Returns a stop function that should be called on shutdown to finalize profiles. +func InitPProf(log *logging.Logger, mode string, addr string) func() { //nolint:gocyclo + noop := func() {} + + switch mode { + case "http": + go func() { + mux := http.NewServeMux() + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + + for _, profile := range []string{"heap", "goroutine", "threadcreate", "block", "mutex", "allocs"} { + mux.Handle("/debug/pprof/"+profile, pprof.Handler(profile)) + } + + srv := &http.Server{ + Addr: addr, + Handler: mux, + ReadHeaderTimeout: 5 * time.Second, + WriteTimeout: 30 * time.Second, + } + log.Infof("Serving pprof on http://%s", addr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Errorf("pprof http server failed: %v", err) + } + }() + + time.Sleep(100 * time.Millisecond) + return noop + + case "cpu": + f, err := os.Create("cpu.pprof") + if err != nil { + log.Errorf("failed to create cpu.pprof: %v", err) + return noop + } + if err := rpprof.StartCPUProfile(f); err != nil { + log.Errorf("failed to start CPU profile: %v", err) + if err := f.Close(); err != nil { + log.Errorf("failed to close cpu.pprof: %v", err) + } + return noop + } + log.Info("CPU profiling started, will write to cpu.pprof on shutdown") + return func() { + rpprof.StopCPUProfile() + if err := f.Close(); err != nil { + log.Errorf("failed to close cpu.pprof: %v", err) + } + log.Info("CPU profile written to cpu.pprof") + } + + case "mem": + log.Info("Memory profiling enabled, will write to mem.pprof on shutdown") + return func() { + f, err := os.Create("mem.pprof") + if err != nil { + log.Errorf("failed to create mem.pprof: %v", err) + return + } + defer f.Close() //nolint:errcheck,gosec + runtime.GC() + if err := rpprof.WriteHeapProfile(f); err != nil { + log.Errorf("failed to write memory profile: %v", err) + return + } + log.Info("Memory profile written to mem.pprof") + } + + case "mutex": + runtime.SetMutexProfileFraction(1) + log.Info("Mutex profiling enabled, will write to mutex.pprof on shutdown") + return func() { + f, err := os.Create("mutex.pprof") + if err != nil { + log.Errorf("failed to create mutex.pprof: %v", err) + return + } + defer f.Close() //nolint:errcheck,gosec + if err := rpprof.Lookup("mutex").WriteTo(f, 0); err != nil { + log.Errorf("failed to write mutex profile: %v", err) + return + } + log.Info("Mutex profile written to mutex.pprof") + } + + case "block": + runtime.SetBlockProfileRate(1) + log.Info("Block profiling enabled, will write to block.pprof on shutdown") + return func() { + f, err := os.Create("block.pprof") + if err != nil { + log.Errorf("failed to create block.pprof: %v", err) + return + } + defer f.Close() //nolint:errcheck,gosec + if err := rpprof.Lookup("block").WriteTo(f, 0); err != nil { + log.Errorf("failed to write block profile: %v", err) + return + } + log.Info("Block profile written to block.pprof") + } + + case "trace": + go func() { + mux := http.NewServeMux() + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + srv := &http.Server{ + Addr: addr, + Handler: mux, + ReadHeaderTimeout: 5 * time.Second, + WriteTimeout: 60 * time.Second, + } + log.Infof("Serving trace endpoint on http://%s/debug/pprof/trace", addr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Errorf("pprof trace server failed: %v", err) + } + }() + + time.Sleep(100 * time.Millisecond) + return noop + + case "": + // no-op + + default: + log.Errorf("unknown pprof mode %q, supported: [ cpu | mem | mutex | block | trace | http ]", mode) + } + + return noop +} diff --git a/vendor/github.com/pelletier/go-toml/v2/.gitignore b/vendor/github.com/pelletier/go-toml/v2/.gitignore index 4b7c4eda3..eaf580dfd 100644 --- a/vendor/github.com/pelletier/go-toml/v2/.gitignore +++ b/vendor/github.com/pelletier/go-toml/v2/.gitignore @@ -5,3 +5,4 @@ cmd/tomljson/tomljson cmd/tomltestgen/tomltestgen dist tests/ +test-results diff --git a/vendor/github.com/pelletier/go-toml/v2/.golangci.toml b/vendor/github.com/pelletier/go-toml/v2/.golangci.toml index 067db5517..7d2e5b04c 100644 --- a/vendor/github.com/pelletier/go-toml/v2/.golangci.toml +++ b/vendor/github.com/pelletier/go-toml/v2/.golangci.toml @@ -1,84 +1,76 @@ -[service] -golangci-lint-version = "1.39.0" - -[linters-settings.wsl] -allow-assign-and-anything = true - -[linters-settings.exhaustive] -default-signifies-exhaustive = true +version = "2" [linters] -disable-all = true +default = "none" enable = [ "asciicheck", "bodyclose", - "cyclop", - "deadcode", - "depguard", "dogsled", "dupl", "durationcheck", "errcheck", "errorlint", "exhaustive", - # "exhaustivestruct", - "exportloopref", "forbidigo", - # "forcetypeassert", - "funlen", - "gci", - # "gochecknoglobals", "gochecknoinits", - "gocognit", "goconst", "gocritic", - "gocyclo", - "godot", - "godox", - # "goerr113", - "gofmt", - "gofumpt", + "godoclint", "goheader", - "goimports", - "golint", - "gomnd", - # "gomoddirectives", "gomodguard", "goprintffuncname", "gosec", - "gosimple", "govet", - # "ifshort", "importas", "ineffassign", "lll", "makezero", + "mirror", "misspell", "nakedret", - "nestif", "nilerr", - # "nlreturn", "noctx", "nolintlint", - #"paralleltest", + "perfsprint", "prealloc", "predeclared", "revive", "rowserrcheck", "sqlclosecheck", "staticcheck", - "structcheck", - "stylecheck", - # "testpackage", "thelper", "tparallel", - "typecheck", "unconvert", "unparam", "unused", - "varcheck", + "usetesting", "wastedassign", "whitespace", - # "wrapcheck", - # "wsl" +] + +[linters.settings.exhaustive] +default-signifies-exhaustive = true + +[linters.settings.lll] +line-length = 150 + +[[linters.exclusions.rules]] +path = ".test.go" +linters = ["goconst", "gosec"] + +[[linters.exclusions.rules]] +path = "main.go" +linters = ["forbidigo"] + +[[linters.exclusions.rules]] +path = "internal" +linters = ["revive"] +text = "(exported|indent-error-flow): " + +[formatters] +enable = [ + "gci", + "gofmt", + "gofumpt", + "goimports", ] diff --git a/vendor/github.com/pelletier/go-toml/v2/.goreleaser.yaml b/vendor/github.com/pelletier/go-toml/v2/.goreleaser.yaml index 47f0f5914..3e19ea710 100644 --- a/vendor/github.com/pelletier/go-toml/v2/.goreleaser.yaml +++ b/vendor/github.com/pelletier/go-toml/v2/.goreleaser.yaml @@ -22,7 +22,6 @@ builds: - linux_riscv64 - windows_amd64 - windows_arm64 - - windows_arm - darwin_amd64 - darwin_arm64 - id: tomljson @@ -42,7 +41,6 @@ builds: - linux_riscv64 - windows_amd64 - windows_arm64 - - windows_arm - darwin_amd64 - darwin_arm64 - id: jsontoml @@ -62,7 +60,6 @@ builds: - linux_arm - windows_amd64 - windows_arm64 - - windows_arm - darwin_amd64 - darwin_arm64 universal_binaries: diff --git a/vendor/github.com/pelletier/go-toml/v2/AGENTS.md b/vendor/github.com/pelletier/go-toml/v2/AGENTS.md new file mode 100644 index 000000000..dafe44d76 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/AGENTS.md @@ -0,0 +1,64 @@ +# Agent Guidelines for go-toml + +This file provides guidelines for AI agents contributing to go-toml. All agents must follow these rules derived from [CONTRIBUTING.md](./CONTRIBUTING.md). + +## Project Overview + +go-toml is a TOML library for Go. The goal is to provide an easy-to-use and efficient TOML implementation that gets the job done without getting in the way. + +## Code Change Rules + +### Backward Compatibility + +- **No backward-incompatible changes** unless explicitly discussed and approved +- Avoid breaking people's programs unless absolutely necessary + +### Testing Requirements + +- **All bug fixes must include regression tests** +- **All new code must be tested** +- Run tests before submitting: `go test -race ./...` +- Test coverage must not decrease. Check with: + ```bash + go test -covermode=atomic -coverprofile=coverage.out + go tool cover -func=coverage.out + ``` +- All lines of code touched by changes should be covered by tests + +### Performance Requirements + +- go-toml aims to stay efficient; avoid performance regressions +- Run benchmarks to verify: `go test ./... -bench=. -count=10` +- Compare results using [benchstat](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat) + +### Documentation + +- New features or feature extensions must include documentation +- Documentation lives in [README.md](./README.md) and throughout source code + +### Code Style + +- Follow existing code format and structure +- Code must pass `go fmt` +- Code must pass linting with the same golangci-lint version as CI (see version in `.github/workflows/lint.yml`): + ```bash + # Install specific version (check lint.yml for current version) + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin + # Run linter + golangci-lint run ./... + ``` + +### Commit Messages + +- Commit messages must explain **why** the change is needed +- Keep messages clear and informative even if details are in the PR description + +## Pull Request Checklist + +Before submitting: + +1. Tests pass (`go test -race ./...`) +2. No backward-incompatible changes (unless discussed) +3. Relevant documentation added/updated +4. No performance regression (verify with benchmarks) +5. Title is clear and understandable for changelog diff --git a/vendor/github.com/pelletier/go-toml/v2/CONTRIBUTING.md b/vendor/github.com/pelletier/go-toml/v2/CONTRIBUTING.md index 96ecf9e2b..28b88ec33 100644 --- a/vendor/github.com/pelletier/go-toml/v2/CONTRIBUTING.md +++ b/vendor/github.com/pelletier/go-toml/v2/CONTRIBUTING.md @@ -33,7 +33,7 @@ The documentation is present in the [README][readme] and thorough the source code. On release, it gets updated on [pkg.go.dev][pkg.go.dev]. To make a change to the documentation, create a pull request with your proposed changes. For simple changes like that, the easiest way to go is probably the "Fork this -project and edit the file" button on Github, displayed at the top right of the +project and edit the file" button on GitHub, displayed at the top right of the file. Unless it's a trivial change (for example a typo), provide a little bit of context in your pull request description or commit message. @@ -92,6 +92,48 @@ However, given GitHub's new policy to _not_ run Actions on pull requests until a maintainer clicks on button, it is highly recommended that you run them locally as you make changes. +### Test across Go versions + +The repository includes tooling to test go-toml across multiple Go versions +(1.11 through 1.25) both locally and in GitHub Actions. + +#### Local testing with Docker + +Prerequisites: Docker installed and running, Bash shell, `rsync` command. + +```bash +# Test all Go versions in parallel (default) +./test-go-versions.sh + +# Test specific versions +./test-go-versions.sh 1.21 1.22 1.23 + +# Test sequentially (slower but uses less resources) +./test-go-versions.sh --sequential + +# Verbose output with custom results directory +./test-go-versions.sh --verbose --output ./my-results 1.24 1.25 + +# Show all options +./test-go-versions.sh --help +``` + +The script creates Docker containers for each Go version and runs the full test +suite. Results are saved to a `test-results/` directory with individual logs and +a comprehensive summary report. + +The script only exits with a non-zero status code if either of the two most +recent Go versions fail. + +#### GitHub Actions testing (maintainers) + +1. Go to the **Actions** tab in the GitHub repository +2. Select **"Go Versions Compatibility Test"** from the workflow list +3. Click **"Run workflow"** +4. Optionally customize: + - **Go versions**: Space-separated list (e.g., `1.21 1.22 1.23`) + - **Execution mode**: Parallel (faster) or sequential (more stable) + ### Check coverage We use `go tool cover` to compute test coverage. Most code editors have a way to @@ -111,7 +153,7 @@ code lowers the coverage. Go-toml aims to stay efficient. We rely on a set of scenarios executed with Go's builtin benchmark systems. Because of their noisy nature, containers provided by -Github Actions cannot be reliably used for benchmarking. As a result, you are +GitHub Actions cannot be reliably used for benchmarking. As a result, you are responsible for checking that your changes do not incur a performance penalty. You can run their following to execute benchmarks: @@ -168,13 +210,13 @@ Checklist: 1. Decide on the next version number. Use semver. Review commits since last version to assess. 2. Tag release. For example: -``` -git checkout v2 -git pull -git tag v2.2.0 -git push --tags -``` -3. CI automatically builds a draft Github release. Review it and edit as + ``` + git checkout v2 + git pull + git tag v2.2.0 + git push --tags + ``` +3. CI automatically builds a draft GitHub release. Review it and edit as necessary. Look for "Other changes". That would indicate a pull request not labeled properly. Tweak labels and pull request titles until changelog looks good for users. diff --git a/vendor/github.com/pelletier/go-toml/v2/README.md b/vendor/github.com/pelletier/go-toml/v2/README.md index 0755e5564..61cdd181f 100644 --- a/vendor/github.com/pelletier/go-toml/v2/README.md +++ b/vendor/github.com/pelletier/go-toml/v2/README.md @@ -107,7 +107,11 @@ type MyConfig struct { ### Unmarshaling [`Unmarshal`][unmarshal] reads a TOML document and fills a Go structure with its -content. For example: +content. + +Note that the struct variable names are _capitalized_, while the variables in the toml document are _lowercase_. + +For example: ```go doc := ` @@ -133,6 +137,62 @@ fmt.Println("tags:", cfg.Tags) [unmarshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Unmarshal + +Here is an example using tables with some simple nesting: + +```go +doc := ` +age = 45 +fruits = ["apple", "pear"] + +# these are very important! +[my-variables] +first = 1 +second = 0.2 +third = "abc" + +# this is not so important. +[my-variables.b] +bfirst = 123 +` + +var Document struct { + Age int + Fruits []string + + Myvariables struct { + First int + Second float64 + Third string + + B struct { + Bfirst int + } + } `toml:"my-variables"` +} + +err := toml.Unmarshal([]byte(doc), &Document) +if err != nil { + panic(err) +} + +fmt.Println("age:", Document.Age) +fmt.Println("fruits:", Document.Fruits) +fmt.Println("my-variables.first:", Document.Myvariables.First) +fmt.Println("my-variables.second:", Document.Myvariables.Second) +fmt.Println("my-variables.third:", Document.Myvariables.Third) +fmt.Println("my-variables.B.Bfirst:", Document.Myvariables.B.Bfirst) + +// Output: +// age: 45 +// fruits: [apple pear] +// my-variables.first: 1 +// my-variables.second: 0.2 +// my-variables.third: abc +// my-variables.B.Bfirst: 123 +``` + + ### Marshaling [`Marshal`][marshal] is the opposite of Unmarshal: it represents a Go structure @@ -175,17 +235,17 @@ the AST level. See https://pkg.go.dev/github.com/pelletier/go-toml/v2/unstable. Execution time speedup compared to other Go TOML libraries: - - - - - - - - - - - + + + + + + + + + + +
Benchmarkgo-toml v1BurntSushi/toml
Marshal/HugoFrontMatter-21.9x2.2x
Marshal/ReferenceFile/map-21.7x2.1x
Marshal/ReferenceFile/struct-22.2x3.0x
Unmarshal/HugoFrontMatter-22.9x2.7x
Unmarshal/ReferenceFile/map-22.6x2.7x
Unmarshal/ReferenceFile/struct-24.6x5.1x
Benchmarkgo-toml v1BurntSushi/toml
Marshal/HugoFrontMatter-22.1x2.0x
Marshal/ReferenceFile/map-22.0x2.0x
Marshal/ReferenceFile/struct-22.3x2.5x
Unmarshal/HugoFrontMatter-23.3x2.8x
Unmarshal/ReferenceFile/map-22.9x3.0x
Unmarshal/ReferenceFile/struct-24.8x5.0x
See more

The table above has the results of the most common use-cases. The table below @@ -193,22 +253,22 @@ contains the results of all benchmarks, including unrealistic ones. It is provided for completeness.

- - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
Benchmarkgo-toml v1BurntSushi/toml
Marshal/SimpleDocument/map-21.8x2.7x
Marshal/SimpleDocument/struct-22.7x3.8x
Unmarshal/SimpleDocument/map-23.8x3.0x
Unmarshal/SimpleDocument/struct-25.6x4.1x
UnmarshalDataset/example-23.0x3.2x
UnmarshalDataset/code-22.3x2.9x
UnmarshalDataset/twitter-22.6x2.7x
UnmarshalDataset/citm_catalog-22.2x2.3x
UnmarshalDataset/canada-21.8x1.5x
UnmarshalDataset/config-24.1x2.9x
geomean2.7x2.8x
Benchmarkgo-toml v1BurntSushi/toml
Marshal/SimpleDocument/map-22.0x2.9x
Marshal/SimpleDocument/struct-22.5x3.6x
Unmarshal/SimpleDocument/map-24.2x3.4x
Unmarshal/SimpleDocument/struct-25.9x4.4x
UnmarshalDataset/example-23.2x2.9x
UnmarshalDataset/code-22.4x2.8x
UnmarshalDataset/twitter-22.7x2.5x
UnmarshalDataset/citm_catalog-22.3x2.3x
UnmarshalDataset/canada-21.9x1.5x
UnmarshalDataset/config-25.4x3.0x
geomean2.9x2.8x

This table can be generated with ./ci.sh benchmark -a -html.

diff --git a/vendor/github.com/pelletier/go-toml/v2/ci.sh b/vendor/github.com/pelletier/go-toml/v2/ci.sh index 86217a9b0..30c23d1a1 100644 --- a/vendor/github.com/pelletier/go-toml/v2/ci.sh +++ b/vendor/github.com/pelletier/go-toml/v2/ci.sh @@ -147,7 +147,7 @@ bench() { pushd "$dir" if [ "${replace}" != "" ]; then - find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2|${replace}|g" {} \; + find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2\"|${replace}\"|g" {} \; go get "${replace}" fi @@ -195,6 +195,11 @@ for line in reversed(lines[2:]): "%.1fx" % (float(line[3])/v2), # v1 "%.1fx" % (float(line[7])/v2), # bs ]) + +if not results: + print("No benchmark results to display.", file=sys.stderr) + sys.exit(1) + # move geomean to the end results.append(results[0]) del results[0] diff --git a/vendor/github.com/pelletier/go-toml/v2/decode.go b/vendor/github.com/pelletier/go-toml/v2/decode.go index f0ec3b170..f3f14eff1 100644 --- a/vendor/github.com/pelletier/go-toml/v2/decode.go +++ b/vendor/github.com/pelletier/go-toml/v2/decode.go @@ -230,8 +230,8 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) { return t, nil, err } - if t.Second > 60 { - return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater 60") + if t.Second > 59 { + return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater than 59") } b = b[8:] @@ -279,7 +279,6 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) { return t, b, nil } -//nolint:cyclop func parseFloat(b []byte) (float64, error) { if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' { return math.NaN(), nil diff --git a/vendor/github.com/pelletier/go-toml/v2/errors.go b/vendor/github.com/pelletier/go-toml/v2/errors.go index 309733f1f..d68835dfa 100644 --- a/vendor/github.com/pelletier/go-toml/v2/errors.go +++ b/vendor/github.com/pelletier/go-toml/v2/errors.go @@ -2,10 +2,10 @@ package toml import ( "fmt" + "reflect" "strconv" "strings" - "github.com/pelletier/go-toml/v2/internal/danger" "github.com/pelletier/go-toml/v2/unstable" ) @@ -54,6 +54,18 @@ func (s *StrictMissingError) String() string { return buf.String() } +// Unwrap returns wrapped decode errors +// +// Implements errors.Join() interface. +func (s *StrictMissingError) Unwrap() []error { + errs := make([]error, len(s.Errors)) + for i := range s.Errors { + errs[i] = &s.Errors[i] + } + return errs +} + +// Key represents a TOML key as a sequence of key parts. type Key []string // Error returns the error message contained in the DecodeError. @@ -78,7 +90,7 @@ func (e *DecodeError) Key() Key { return e.key } -// decodeErrorFromHighlight creates a DecodeError referencing a highlighted +// wrapDecodeError creates a DecodeError referencing a highlighted // range of bytes from document. // // highlight needs to be a sub-slice of document, or this function panics. @@ -88,7 +100,7 @@ func (e *DecodeError) Key() Key { // //nolint:funlen func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError { - offset := danger.SubsliceOffset(document, de.Highlight) + offset := subsliceOffset(document, de.Highlight) errMessage := de.Error() errLine, errColumn := positionAtEnd(document[:offset]) @@ -248,5 +260,24 @@ func positionAtEnd(b []byte) (row int, column int) { } } - return + return row, column +} + +// subsliceOffset returns the byte offset of subslice within data. +// subslice must share the same backing array as data. +func subsliceOffset(data []byte, subslice []byte) int { + if len(subslice) == 0 { + return 0 + } + + // Use reflect to get the data pointers of both slices. + // This is safe because we're only reading the pointer values for comparison. + dataPtr := reflect.ValueOf(data).Pointer() + subPtr := reflect.ValueOf(subslice).Pointer() + + offset := int(subPtr - dataPtr) + if offset < 0 || offset > len(data) { + panic("subslice is not within data") + } + return offset } diff --git a/vendor/github.com/pelletier/go-toml/v2/internal/characters/ascii.go b/vendor/github.com/pelletier/go-toml/v2/internal/characters/ascii.go index 80f698db4..50a6d1702 100644 --- a/vendor/github.com/pelletier/go-toml/v2/internal/characters/ascii.go +++ b/vendor/github.com/pelletier/go-toml/v2/internal/characters/ascii.go @@ -1,6 +1,6 @@ package characters -var invalidAsciiTable = [256]bool{ +var invalidASCIITable = [256]bool{ 0x00: true, 0x01: true, 0x02: true, @@ -37,6 +37,6 @@ var invalidAsciiTable = [256]bool{ 0x7F: true, } -func InvalidAscii(b byte) bool { - return invalidAsciiTable[b] +func InvalidASCII(b byte) bool { + return invalidASCIITable[b] } diff --git a/vendor/github.com/pelletier/go-toml/v2/internal/characters/utf8.go b/vendor/github.com/pelletier/go-toml/v2/internal/characters/utf8.go index db4f45acb..7c5cb55e4 100644 --- a/vendor/github.com/pelletier/go-toml/v2/internal/characters/utf8.go +++ b/vendor/github.com/pelletier/go-toml/v2/internal/characters/utf8.go @@ -1,20 +1,12 @@ +// Package characters provides functions for working with string encodings. package characters import ( "unicode/utf8" ) -type utf8Err struct { - Index int - Size int -} - -func (u utf8Err) Zero() bool { - return u.Size == 0 -} - -// Verified that a given string is only made of valid UTF-8 characters allowed -// by the TOML spec: +// Utf8TomlValidAlreadyEscaped verifies that a given string is only made of +// valid UTF-8 characters allowed by the TOML spec: // // Any Unicode character may be used except those that must be escaped: // quotation mark, backslash, and the control characters other than tab (U+0000 @@ -23,8 +15,8 @@ func (u utf8Err) Zero() bool { // It is a copy of the Go 1.17 utf8.Valid implementation, tweaked to exit early // when a character is not allowed. // -// The returned utf8Err is Zero() if the string is valid, or contains the byte -// index and size of the invalid character. +// The returned slice is empty if the string is valid, or contains the bytes +// of the invalid character. // // quotation mark => already checked // backslash => already checked @@ -32,9 +24,8 @@ func (u utf8Err) Zero() bool { // 0x9 => tab, ok // 0xA - 0x1F => invalid // 0x7F => invalid -func Utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) { +func Utf8TomlValidAlreadyEscaped(p []byte) []byte { // Fast path. Check for and skip 8 bytes of ASCII characters per iteration. - offset := 0 for len(p) >= 8 { // Combining two 32 bit loads allows the same code to be used // for 32 and 64 bit platforms. @@ -48,24 +39,19 @@ func Utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) { } for i, b := range p[:8] { - if InvalidAscii(b) { - err.Index = offset + i - err.Size = 1 - return + if InvalidASCII(b) { + return p[i : i+1] } } p = p[8:] - offset += 8 } n := len(p) for i := 0; i < n; { pi := p[i] if pi < utf8.RuneSelf { - if InvalidAscii(pi) { - err.Index = offset + i - err.Size = 1 - return + if InvalidASCII(pi) { + return p[i : i+1] } i++ continue @@ -73,44 +59,34 @@ func Utf8TomlValidAlreadyEscaped(p []byte) (err utf8Err) { x := first[pi] if x == xx { // Illegal starter byte. - err.Index = offset + i - err.Size = 1 - return + return p[i : i+1] } size := int(x & 7) if i+size > n { // Short or invalid. - err.Index = offset + i - err.Size = n - i - return + return p[i:n] } accept := acceptRanges[x>>4] if c := p[i+1]; c < accept.lo || accept.hi < c { - err.Index = offset + i - err.Size = 2 - return - } else if size == 2 { + return p[i : i+2] + } else if size == 2 { //revive:disable:empty-block } else if c := p[i+2]; c < locb || hicb < c { - err.Index = offset + i - err.Size = 3 - return - } else if size == 3 { + return p[i : i+3] + } else if size == 3 { //revive:disable:empty-block } else if c := p[i+3]; c < locb || hicb < c { - err.Index = offset + i - err.Size = 4 - return + return p[i : i+4] } i += size } - return + return nil } -// Return the size of the next rune if valid, 0 otherwise. +// Utf8ValidNext returns the size of the next rune if valid, 0 otherwise. func Utf8ValidNext(p []byte) int { c := p[0] if c < utf8.RuneSelf { - if InvalidAscii(c) { + if InvalidASCII(c) { return 0 } return 1 @@ -129,10 +105,10 @@ func Utf8ValidNext(p []byte) int { accept := acceptRanges[x>>4] if c := p[1]; c < accept.lo || accept.hi < c { return 0 - } else if size == 2 { + } else if size == 2 { //nolint:revive } else if c := p[2]; c < locb || hicb < c { return 0 - } else if size == 3 { + } else if size == 3 { //nolint:revive } else if c := p[3]; c < locb || hicb < c { return 0 } diff --git a/vendor/github.com/pelletier/go-toml/v2/internal/danger/danger.go b/vendor/github.com/pelletier/go-toml/v2/internal/danger/danger.go deleted file mode 100644 index e38e1131b..000000000 --- a/vendor/github.com/pelletier/go-toml/v2/internal/danger/danger.go +++ /dev/null @@ -1,65 +0,0 @@ -package danger - -import ( - "fmt" - "reflect" - "unsafe" -) - -const maxInt = uintptr(int(^uint(0) >> 1)) - -func SubsliceOffset(data []byte, subslice []byte) int { - datap := (*reflect.SliceHeader)(unsafe.Pointer(&data)) - hlp := (*reflect.SliceHeader)(unsafe.Pointer(&subslice)) - - if hlp.Data < datap.Data { - panic(fmt.Errorf("subslice address (%d) is before data address (%d)", hlp.Data, datap.Data)) - } - offset := hlp.Data - datap.Data - - if offset > maxInt { - panic(fmt.Errorf("slice offset larger than int (%d)", offset)) - } - - intoffset := int(offset) - - if intoffset > datap.Len { - panic(fmt.Errorf("slice offset (%d) is farther than data length (%d)", intoffset, datap.Len)) - } - - if intoffset+hlp.Len > datap.Len { - panic(fmt.Errorf("slice ends (%d+%d) is farther than data length (%d)", intoffset, hlp.Len, datap.Len)) - } - - return intoffset -} - -func BytesRange(start []byte, end []byte) []byte { - if start == nil || end == nil { - panic("cannot call BytesRange with nil") - } - startp := (*reflect.SliceHeader)(unsafe.Pointer(&start)) - endp := (*reflect.SliceHeader)(unsafe.Pointer(&end)) - - if startp.Data > endp.Data { - panic(fmt.Errorf("start pointer address (%d) is after end pointer address (%d)", startp.Data, endp.Data)) - } - - l := startp.Len - endLen := int(endp.Data-startp.Data) + endp.Len - if endLen > l { - l = endLen - } - - if l > startp.Cap { - panic(fmt.Errorf("range length is larger than capacity")) - } - - return start[:l] -} - -func Stride(ptr unsafe.Pointer, size uintptr, offset int) unsafe.Pointer { - // TODO: replace with unsafe.Add when Go 1.17 is released - // https://github.com/golang/go/issues/40481 - return unsafe.Pointer(uintptr(ptr) + uintptr(int(size)*offset)) -} diff --git a/vendor/github.com/pelletier/go-toml/v2/internal/danger/typeid.go b/vendor/github.com/pelletier/go-toml/v2/internal/danger/typeid.go deleted file mode 100644 index 9d41c28a2..000000000 --- a/vendor/github.com/pelletier/go-toml/v2/internal/danger/typeid.go +++ /dev/null @@ -1,23 +0,0 @@ -package danger - -import ( - "reflect" - "unsafe" -) - -// typeID is used as key in encoder and decoder caches to enable using -// the optimize runtime.mapaccess2_fast64 function instead of the more -// expensive lookup if we were to use reflect.Type as map key. -// -// typeID holds the pointer to the reflect.Type value, which is unique -// in the program. -// -// https://github.com/segmentio/encoding/blob/master/json/codec.go#L59-L61 -type TypeID unsafe.Pointer - -func MakeTypeID(t reflect.Type) TypeID { - // reflect.Type has the fields: - // typ unsafe.Pointer - // ptr unsafe.Pointer - return TypeID((*[2]unsafe.Pointer)(unsafe.Pointer(&t))[1]) -} diff --git a/vendor/github.com/pelletier/go-toml/v2/internal/tracker/key.go b/vendor/github.com/pelletier/go-toml/v2/internal/tracker/key.go index 149b17f53..6344fd047 100644 --- a/vendor/github.com/pelletier/go-toml/v2/internal/tracker/key.go +++ b/vendor/github.com/pelletier/go-toml/v2/internal/tracker/key.go @@ -36,7 +36,7 @@ func (t *KeyTracker) Pop(node *unstable.Node) { } } -// Key returns the current key +// Key returns the current key. func (t *KeyTracker) Key() []string { k := make([]string, len(t.k)) copy(k, t.k) diff --git a/vendor/github.com/pelletier/go-toml/v2/internal/tracker/seen.go b/vendor/github.com/pelletier/go-toml/v2/internal/tracker/seen.go index 76df2d5b6..206235800 100644 --- a/vendor/github.com/pelletier/go-toml/v2/internal/tracker/seen.go +++ b/vendor/github.com/pelletier/go-toml/v2/internal/tracker/seen.go @@ -288,11 +288,12 @@ func (s *SeenTracker) checkKeyValue(node *unstable.Node) (bool, error) { idx = s.create(parentIdx, k, tableKind, false, true) } else { entry := s.entries[idx] - if it.IsLast() { + switch { + case it.IsLast(): return false, fmt.Errorf("toml: key %s is already defined", string(k)) - } else if entry.kind != tableKind { + case entry.kind != tableKind: return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind) - } else if entry.explicit { + case entry.explicit: return false, fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k)) } } @@ -309,16 +310,16 @@ func (s *SeenTracker) checkKeyValue(node *unstable.Node) (bool, error) { return s.checkInlineTable(value) case unstable.Array: return s.checkArray(value) + default: + return false, nil } - - return false, nil } func (s *SeenTracker) checkArray(node *unstable.Node) (first bool, err error) { it := node.Children() for it.Next() { n := it.Node() - switch n.Kind { + switch n.Kind { //nolint:exhaustive case unstable.InlineTable: first, err = s.checkInlineTable(n) if err != nil { diff --git a/vendor/github.com/pelletier/go-toml/v2/internal/tracker/tracker.go b/vendor/github.com/pelletier/go-toml/v2/internal/tracker/tracker.go index bf0317392..ed510382c 100644 --- a/vendor/github.com/pelletier/go-toml/v2/internal/tracker/tracker.go +++ b/vendor/github.com/pelletier/go-toml/v2/internal/tracker/tracker.go @@ -1 +1,2 @@ +// Package tracker provides functions for keeping track of AST nodes. package tracker diff --git a/vendor/github.com/pelletier/go-toml/v2/localtime.go b/vendor/github.com/pelletier/go-toml/v2/localtime.go index a856bfdb0..502ef2f2f 100644 --- a/vendor/github.com/pelletier/go-toml/v2/localtime.go +++ b/vendor/github.com/pelletier/go-toml/v2/localtime.go @@ -45,7 +45,7 @@ func (d *LocalDate) UnmarshalText(b []byte) error { type LocalTime struct { Hour int // Hour of the day: [0; 24[ Minute int // Minute of the hour: [0; 60[ - Second int // Second of the minute: [0; 60[ + Second int // Second of the minute: [0; 59] Nanosecond int // Nanoseconds within the second: [0, 1000000000[ Precision int // Number of digits to display for Nanosecond. } diff --git a/vendor/github.com/pelletier/go-toml/v2/marshaler.go b/vendor/github.com/pelletier/go-toml/v2/marshaler.go index 161acd934..ca462d40e 100644 --- a/vendor/github.com/pelletier/go-toml/v2/marshaler.go +++ b/vendor/github.com/pelletier/go-toml/v2/marshaler.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding" "encoding/json" + "errors" "fmt" "io" "math" @@ -42,7 +43,7 @@ type Encoder struct { arraysMultiline bool indentSymbol string indentTables bool - marshalJsonNumbers bool + marshalJSONNumbers bool } // NewEncoder returns a new Encoder that writes to w. @@ -89,14 +90,14 @@ func (enc *Encoder) SetIndentTables(indent bool) *Encoder { return enc } -// SetMarshalJsonNumbers forces the encoder to serialize `json.Number` as a +// SetMarshalJSONNumbers forces the encoder to serialize `json.Number` as a // float or integer instead of relying on TextMarshaler to emit a string. // // *Unstable:* This method does not follow the compatibility guarantees of // semver. It can be changed or removed without a new major version being // issued. -func (enc *Encoder) SetMarshalJsonNumbers(indent bool) *Encoder { - enc.marshalJsonNumbers = indent +func (enc *Encoder) SetMarshalJSONNumbers(indent bool) *Encoder { + enc.marshalJSONNumbers = indent return enc } @@ -161,6 +162,8 @@ func (enc *Encoder) SetMarshalJsonNumbers(indent bool) *Encoder { // // The "omitempty" option prevents empty values or groups from being emitted. // +// The "omitzero" option prevents zero values or groups from being emitted. +// // The "commented" option prefixes the value and all its children with a comment // symbol. // @@ -177,7 +180,7 @@ func (enc *Encoder) Encode(v interface{}) error { ctx.inline = enc.tablesInline if v == nil { - return fmt.Errorf("toml: cannot encode a nil interface") + return errors.New("toml: cannot encode a nil interface") } b, err := enc.encode(b, ctx, reflect.ValueOf(v)) @@ -196,6 +199,7 @@ func (enc *Encoder) Encode(v interface{}) error { type valueOptions struct { multiline bool omitempty bool + omitzero bool commented bool comment string } @@ -266,16 +270,15 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e case LocalDateTime: return append(b, x.String()...), nil case json.Number: - if enc.marshalJsonNumbers { + if enc.marshalJSONNumbers { if x == "" { /// Useful zero value. return append(b, "0"...), nil } else if v, err := x.Int64(); err == nil { return enc.encode(b, ctx, reflect.ValueOf(v)) } else if f, err := x.Float64(); err == nil { return enc.encode(b, ctx, reflect.ValueOf(f)) - } else { - return nil, fmt.Errorf("toml: unable to convert %q to int64 or float64", x) } + return nil, fmt.Errorf("toml: unable to convert %q to int64 or float64", x) } } @@ -309,7 +312,7 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e return enc.encodeSlice(b, ctx, v) case reflect.Interface: if v.IsNil() { - return nil, fmt.Errorf("toml: encoding a nil interface is not supported") + return nil, errors.New("toml: encoding a nil interface is not supported") } return enc.encode(b, ctx, v.Elem()) @@ -326,28 +329,30 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e case reflect.Float32: f := v.Float() - if math.IsNaN(f) { + switch { + case math.IsNaN(f): b = append(b, "nan"...) - } else if f > math.MaxFloat32 { + case f > math.MaxFloat32: b = append(b, "inf"...) - } else if f < -math.MaxFloat32 { + case f < -math.MaxFloat32: b = append(b, "-inf"...) - } else if math.Trunc(f) == f { + case math.Trunc(f) == f: b = strconv.AppendFloat(b, f, 'f', 1, 32) - } else { + default: b = strconv.AppendFloat(b, f, 'f', -1, 32) } case reflect.Float64: f := v.Float() - if math.IsNaN(f) { + switch { + case math.IsNaN(f): b = append(b, "nan"...) - } else if f > math.MaxFloat64 { + case f > math.MaxFloat64: b = append(b, "inf"...) - } else if f < -math.MaxFloat64 { + case f < -math.MaxFloat64: b = append(b, "-inf"...) - } else if math.Trunc(f) == f { + case math.Trunc(f) == f: b = strconv.AppendFloat(b, f, 'f', 1, 64) - } else { + default: b = strconv.AppendFloat(b, f, 'f', -1, 64) } case reflect.Bool: @@ -384,6 +389,31 @@ func shouldOmitEmpty(options valueOptions, v reflect.Value) bool { return options.omitempty && isEmptyValue(v) } +func shouldOmitZero(options valueOptions, v reflect.Value) bool { + if !options.omitzero { + return false + } + + // Check if the type implements isZeroer interface (has a custom IsZero method). + if v.Type().Implements(isZeroerType) { + return v.Interface().(isZeroer).IsZero() + } + + // Check if pointer type implements isZeroer. + if reflect.PointerTo(v.Type()).Implements(isZeroerType) { + if v.CanAddr() { + return v.Addr().Interface().(isZeroer).IsZero() + } + // Create a temporary addressable copy to call the pointer receiver method. + pv := reflect.New(v.Type()) + pv.Elem().Set(v) + return pv.Interface().(isZeroer).IsZero() + } + + // Fall back to reflect's IsZero for types without custom IsZero method. + return v.IsZero() +} + func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) { var err error @@ -434,8 +464,9 @@ func isEmptyValue(v reflect.Value) bool { return v.Float() == 0 case reflect.Interface, reflect.Ptr: return v.IsNil() + default: + return false } - return false } func isEmptyStruct(v reflect.Value) bool { @@ -479,7 +510,7 @@ func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byt func needsQuoting(v string) bool { // TODO: vectorize for _, b := range []byte(v) { - if b == '\'' || b == '\r' || b == '\n' || characters.InvalidAscii(b) { + if b == '\'' || b == '\r' || b == '\n' || characters.InvalidASCII(b) { return true } } @@ -517,12 +548,26 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt del = 0x7f ) - for _, r := range []byte(v) { + bv := []byte(v) + for i := 0; i < len(bv); i++ { + r := bv[i] switch r { case '\\': b = append(b, `\\`...) case '"': - b = append(b, `\"`...) + if multiline { + // Quotation marks do not need to be quoted in multiline strings unless + // it contains 3 consecutive. If 3+ quotes appear, quote all of them + // because it's visually better + if i+2 > len(bv) || bv[i+1] != '"' || bv[i+2] != '"' { + b = append(b, r) + } else { + b = append(b, `\"\"\"`...) + i += 2 + } + } else { + b = append(b, `\"`...) + } case '\b': b = append(b, `\b`...) case '\f': @@ -559,9 +604,9 @@ func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte { return append(b, v...) } -func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error) { +func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) []byte { if len(ctx.parentKey) == 0 { - return b, nil + return b } b = enc.encodeComment(ctx.indent, ctx.options.comment, b) @@ -581,10 +626,9 @@ func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error) b = append(b, "]\n"...) - return b, nil + return b } -//nolint:cyclop func (enc *Encoder) encodeKey(b []byte, k string) []byte { needsQuotation := false cannotUseLiteral := false @@ -621,30 +665,33 @@ func (enc *Encoder) encodeKey(b []byte, k string) []byte { func (enc *Encoder) keyToString(k reflect.Value) (string, error) { keyType := k.Type() - switch { - case keyType.Kind() == reflect.String: - return k.String(), nil - - case keyType.Implements(textMarshalerType): + if keyType.Implements(textMarshalerType) { keyB, err := k.Interface().(encoding.TextMarshaler).MarshalText() if err != nil { return "", fmt.Errorf("toml: error marshalling key %v from text: %w", k, err) } return string(keyB), nil + } + + switch keyType.Kind() { + case reflect.String: + return k.String(), nil - case keyType.Kind() == reflect.Int || keyType.Kind() == reflect.Int8 || keyType.Kind() == reflect.Int16 || keyType.Kind() == reflect.Int32 || keyType.Kind() == reflect.Int64: + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(k.Int(), 10), nil - case keyType.Kind() == reflect.Uint || keyType.Kind() == reflect.Uint8 || keyType.Kind() == reflect.Uint16 || keyType.Kind() == reflect.Uint32 || keyType.Kind() == reflect.Uint64: + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return strconv.FormatUint(k.Uint(), 10), nil - case keyType.Kind() == reflect.Float32: + case reflect.Float32: return strconv.FormatFloat(k.Float(), 'f', -1, 32), nil - case keyType.Kind() == reflect.Float64: + case reflect.Float64: return strconv.FormatFloat(k.Float(), 'f', -1, 64), nil + + default: + return "", fmt.Errorf("toml: type %s is not supported as a map key", keyType.Kind()) } - return "", fmt.Errorf("toml: type %s is not supported as a map key", keyType.Kind()) } func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { @@ -657,8 +704,18 @@ func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte for iter.Next() { v := iter.Value() - if isNil(v) { - continue + // Handle nil values: convert nil pointers to zero value, + // skip nil interfaces and nil maps. + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + v = reflect.Zero(v.Type().Elem()) + } + case reflect.Interface, reflect.Map: + if v.IsNil() { + continue + } + default: } k, err := enc.keyToString(iter.Key()) @@ -748,9 +805,8 @@ func walkStruct(ctx encoderCtx, t *table, v reflect.Value) { walkStruct(ctx, t, f.Elem()) } continue - } else { - k = fieldType.Name } + k = fieldType.Name } if isNil(f) { @@ -760,6 +816,7 @@ func walkStruct(ctx encoderCtx, t *table, v reflect.Value) { options := valueOptions{ multiline: opts.multiline, omitempty: opts.omitempty, + omitzero: opts.omitzero, commented: opts.commented, comment: fieldType.Tag.Get("comment"), } @@ -820,6 +877,7 @@ type tagOptions struct { multiline bool inline bool omitempty bool + omitzero bool commented bool } @@ -832,7 +890,7 @@ func parseTag(tag string) (string, tagOptions) { } raw := tag[idx+1:] - tag = string(tag[:idx]) + tag = tag[:idx] for raw != "" { var o string i := strings.Index(raw, ",") @@ -848,6 +906,8 @@ func parseTag(tag string) (string, tagOptions) { opts.inline = true case "omitempty": opts.omitempty = true + case "omitzero": + opts.omitzero = true case "commented": opts.commented = true } @@ -866,10 +926,7 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro } if !ctx.skipTableHeader { - b, err = enc.encodeTableHeader(ctx, b) - if err != nil { - return nil, err - } + b = enc.encodeTableHeader(ctx, b) if enc.indentTables && len(ctx.parentKey) > 0 { ctx.indent++ @@ -882,6 +939,9 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro if shouldOmitEmpty(kv.Options, kv.Value) { continue } + if kv.Options.omitzero && shouldOmitZero(kv.Options, kv.Value) { + continue + } hasNonEmptyKV = true ctx.setKey(kv.Key) @@ -901,6 +961,9 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro if shouldOmitEmpty(table.Options, table.Value) { continue } + if table.Options.omitzero && shouldOmitZero(table.Options, table.Value) { + continue + } if first { first = false if hasNonEmptyKV { @@ -935,6 +998,9 @@ func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte if shouldOmitEmpty(kv.Options, kv.Value) { continue } + if kv.Options.omitzero && shouldOmitZero(kv.Options, kv.Value) { + continue + } if first { first = false @@ -963,11 +1029,14 @@ func willConvertToTable(ctx encoderCtx, v reflect.Value) bool { if !v.IsValid() { return false } - if v.Type() == timeType || v.Type().Implements(textMarshalerType) || (v.Kind() != reflect.Ptr && v.CanAddr() && reflect.PointerTo(v.Type()).Implements(textMarshalerType)) { + t := v.Type() + if t == timeType || t.Implements(textMarshalerType) { + return false + } + if v.Kind() != reflect.Ptr && v.CanAddr() && reflect.PointerTo(t).Implements(textMarshalerType) { return false } - t := v.Type() switch t.Kind() { case reflect.Map, reflect.Struct: return !ctx.inline diff --git a/vendor/github.com/pelletier/go-toml/v2/strict.go b/vendor/github.com/pelletier/go-toml/v2/strict.go index 802e7e4d1..2a147c026 100644 --- a/vendor/github.com/pelletier/go-toml/v2/strict.go +++ b/vendor/github.com/pelletier/go-toml/v2/strict.go @@ -1,7 +1,6 @@ package toml import ( - "github.com/pelletier/go-toml/v2/internal/danger" "github.com/pelletier/go-toml/v2/internal/tracker" "github.com/pelletier/go-toml/v2/unstable" ) @@ -13,6 +12,9 @@ type strict struct { key tracker.KeyTracker missing []unstable.ParserError + + // Reference to the document for computing key ranges. + doc []byte } func (s *strict) EnterTable(node *unstable.Node) { @@ -53,7 +55,7 @@ func (s *strict) MissingTable(node *unstable.Node) { } s.missing = append(s.missing, unstable.ParserError{ - Highlight: keyLocation(node), + Highlight: s.keyLocation(node), Message: "missing table", Key: s.key.Key(), }) @@ -65,7 +67,7 @@ func (s *strict) MissingField(node *unstable.Node) { } s.missing = append(s.missing, unstable.ParserError{ - Highlight: keyLocation(node), + Highlight: s.keyLocation(node), Message: "missing field", Key: s.key.Key(), }) @@ -88,7 +90,7 @@ func (s *strict) Error(doc []byte) error { return err } -func keyLocation(node *unstable.Node) []byte { +func (s *strict) keyLocation(node *unstable.Node) []byte { k := node.Key() hasOne := k.Next() @@ -96,12 +98,17 @@ func keyLocation(node *unstable.Node) []byte { panic("should not be called with empty key") } - start := k.Node().Data - end := k.Node().Data + // Get the range from the first key to the last key. + firstRaw := k.Node().Raw + lastRaw := firstRaw for k.Next() { - end = k.Node().Data + lastRaw = k.Node().Raw } - return danger.BytesRange(start, end) + // Compute the slice from the document using the ranges. + start := firstRaw.Offset + end := lastRaw.Offset + lastRaw.Length + + return s.doc[start:end] } diff --git a/vendor/github.com/pelletier/go-toml/v2/test-go-versions.sh b/vendor/github.com/pelletier/go-toml/v2/test-go-versions.sh new file mode 100644 index 000000000..5fe5c7772 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/test-go-versions.sh @@ -0,0 +1,597 @@ +#!/usr/bin/env bash + +set -uo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Go versions to test (1.11 through 1.26) +GO_VERSIONS=( + "1.11" + "1.12" + "1.13" + "1.14" + "1.15" + "1.16" + "1.17" + "1.18" + "1.19" + "1.20" + "1.21" + "1.22" + "1.23" + "1.24" + "1.25" + "1.26" +) + +# Default values +PARALLEL=true +VERBOSE=false +OUTPUT_DIR="test-results" +DOCKER_TIMEOUT="10m" + +usage() { + cat << EOF +Usage: $0 [OPTIONS] [GO_VERSIONS...] + +Test go-toml across multiple Go versions using Docker containers. + +The script reports the lowest continuous supported Go version (where all subsequent +versions pass) and only exits with non-zero status if either of the two most recent +Go versions fail, indicating immediate attention is needed. + +Note: For Go versions < 1.21, the script automatically updates go.mod to match the +target version, but older versions may still fail due to missing standard library +features (e.g., the 'slices' package introduced in Go 1.21). + +OPTIONS: + -h, --help Show this help message + -s, --sequential Run tests sequentially instead of in parallel + -v, --verbose Enable verbose output + -o, --output DIR Output directory for test results (default: test-results) + -t, --timeout TIME Docker timeout for each test (default: 10m) + --list List available Go versions and exit + +ARGUMENTS: + GO_VERSIONS Specific Go versions to test (default: all supported versions) + Examples: 1.21 1.22 1.23 + +EXAMPLES: + $0 # Test all Go versions in parallel + $0 --sequential # Test all Go versions sequentially + $0 1.21 1.22 1.23 # Test specific versions + $0 --verbose --output ./results 1.25 1.26 # Verbose output to custom directory + +EXIT CODES: + 0 Recent Go versions pass (good compatibility) + 1 Recent Go versions fail (needs attention) or script error + +EOF +} + +log() { + echo -e "${BLUE}[$(date +'%H:%M:%S')]${NC} $*" >&2 +} + +log_success() { + echo -e "${GREEN}[$(date +'%H:%M:%S')] ✓${NC} $*" >&2 +} + +log_error() { + echo -e "${RED}[$(date +'%H:%M:%S')] ✗${NC} $*" >&2 +} + +log_warning() { + echo -e "${YELLOW}[$(date +'%H:%M:%S')] ⚠${NC} $*" >&2 +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + usage + exit 0 + ;; + -s|--sequential) + PARALLEL=false + shift + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + -o|--output) + OUTPUT_DIR="$2" + shift 2 + ;; + -t|--timeout) + DOCKER_TIMEOUT="$2" + shift 2 + ;; + --list) + echo "Available Go versions:" + printf '%s\n' "${GO_VERSIONS[@]}" + exit 0 + ;; + -*) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + *) + # Remaining arguments are Go versions + break + ;; + esac +done + +# If specific versions provided, use those instead of defaults +if [[ $# -gt 0 ]]; then + GO_VERSIONS=("$@") +fi + +# Validate Go versions +for version in "${GO_VERSIONS[@]}"; do + if ! [[ "$version" =~ ^1\.(1[1-9]|2[0-6])$ ]]; then + log_error "Invalid Go version: $version. Supported versions: 1.11-1.26" + exit 1 + fi +done + +# Check if Docker is available +if ! command -v docker &> /dev/null; then + log_error "Docker is required but not installed or not in PATH" + exit 1 +fi + +# Check if Docker daemon is running +if ! docker info &> /dev/null; then + log_error "Docker daemon is not running" + exit 1 +fi + +# Create output directory +mkdir -p "$OUTPUT_DIR" + +# Function to test a single Go version +test_go_version() { + local go_version="$1" + local container_name="go-toml-test-${go_version}" + local result_file="${OUTPUT_DIR}/go-${go_version}.txt" + local dockerfile_content + + log "Testing Go $go_version..." + + # Create a temporary Dockerfile for this version + # For Go versions < 1.21, we need to update go.mod to match the Go version + local needs_go_mod_update=false + if [[ $(echo "$go_version 1.21" | tr ' ' '\n' | sort -V | head -n1) == "$go_version" && "$go_version" != "1.21" ]]; then + needs_go_mod_update=true + fi + + dockerfile_content="FROM golang:${go_version}-alpine + +# Install git (required for go mod) +RUN apk add --no-cache git + +# Set working directory +WORKDIR /app + +# Copy source code +COPY . ." + + # Add go.mod update step for older Go versions + if [[ "$needs_go_mod_update" == true ]]; then + dockerfile_content="$dockerfile_content + +# Update go.mod to match Go version (required for Go < 1.21) +RUN if [ -f go.mod ]; then sed -i 's/^go [0-9]\\+\\.[0-9]\\+\\(\\.[0-9]\\+\\)\\?/go $go_version/' go.mod; fi + +# Note: Go versions < 1.21 may fail due to missing standard library packages (e.g., slices) +# This is expected for projects that use Go 1.21+ features" + fi + + dockerfile_content="$dockerfile_content + +# Run tests +CMD [\"sh\", \"-c\", \"go version && echo '--- Running go test ./... ---' && go test ./...\"]" + + # Create temporary directory for this test + local temp_dir + temp_dir=$(mktemp -d) + + # Copy source to temp directory (excluding test results and git) + rsync -a --exclude="$OUTPUT_DIR" --exclude=".git" --exclude="*.test" . "$temp_dir/" + + # Create Dockerfile in temp directory + echo "$dockerfile_content" > "$temp_dir/Dockerfile" + + # Build and run container + local exit_code=0 + local output + + if $VERBOSE; then + log "Building Docker image for Go $go_version..." + fi + + # Capture both stdout and stderr, and the exit code + if output=$(cd "$temp_dir" && timeout "$DOCKER_TIMEOUT" docker build -t "$container_name" . 2>&1 && \ + timeout "$DOCKER_TIMEOUT" docker run --rm "$container_name" 2>&1); then + log_success "Go $go_version: PASSED" + echo "PASSED" > "${result_file}.status" + else + exit_code=$? + log_error "Go $go_version: FAILED (exit code: $exit_code)" + echo "FAILED" > "${result_file}.status" + fi + + # Save full output + echo "$output" > "$result_file" + + # Clean up + docker rmi "$container_name" &> /dev/null || true + rm -rf "$temp_dir" + + if $VERBOSE; then + echo "--- Go $go_version output ---" + echo "$output" + echo "--- End Go $go_version output ---" + fi + + return $exit_code +} + +# Function to run tests in parallel +run_parallel() { + local pids=() + local failed_versions=() + + log "Starting parallel tests for ${#GO_VERSIONS[@]} Go versions..." + + # Start all tests in background + for version in "${GO_VERSIONS[@]}"; do + test_go_version "$version" & + pids+=($!) + done + + # Wait for all tests to complete + for i in "${!pids[@]}"; do + local pid=${pids[$i]} + local version=${GO_VERSIONS[$i]} + + if ! wait $pid; then + failed_versions+=("$version") + fi + done + + return ${#failed_versions[@]} +} + +# Function to run tests sequentially +run_sequential() { + local failed_versions=() + + log "Starting sequential tests for ${#GO_VERSIONS[@]} Go versions..." + + for version in "${GO_VERSIONS[@]}"; do + if ! test_go_version "$version"; then + failed_versions+=("$version") + fi + done + + return ${#failed_versions[@]} +} + +# Main execution +main() { + local start_time + start_time=$(date +%s) + + log "Starting Go version compatibility tests..." + log "Testing versions: ${GO_VERSIONS[*]}" + log "Output directory: $OUTPUT_DIR" + log "Parallel execution: $PARALLEL" + + local failed_count + if $PARALLEL; then + run_parallel + failed_count=$? + else + run_sequential + failed_count=$? + fi + + local end_time + end_time=$(date +%s) + local duration=$((end_time - start_time)) + + # Collect results for display + local passed_versions=() + local failed_versions=() + local unknown_versions=() + local passed_count=0 + + for version in "${GO_VERSIONS[@]}"; do + local status_file="${OUTPUT_DIR}/go-${version}.txt.status" + if [[ -f "$status_file" ]]; then + local status + status=$(cat "$status_file") + if [[ "$status" == "PASSED" ]]; then + passed_versions+=("$version") + ((passed_count++)) + else + failed_versions+=("$version") + fi + else + unknown_versions+=("$version") + fi + done + + # Generate summary report + local summary_file="${OUTPUT_DIR}/summary.txt" + { + echo "Go Version Compatibility Test Summary" + echo "=====================================" + echo "Date: $(date)" + echo "Duration: ${duration}s" + echo "Parallel: $PARALLEL" + echo "" + echo "Results:" + + for version in "${GO_VERSIONS[@]}"; do + local status_file="${OUTPUT_DIR}/go-${version}.txt.status" + if [[ -f "$status_file" ]]; then + local status + status=$(cat "$status_file") + if [[ "$status" == "PASSED" ]]; then + echo " Go $version: ✓ PASSED" + else + echo " Go $version: ✗ FAILED" + fi + else + echo " Go $version: ? UNKNOWN (no status file)" + fi + done + + echo "" + echo "Summary: $passed_count/${#GO_VERSIONS[@]} versions passed" + + if [[ $failed_count -gt 0 ]]; then + echo "" + echo "Failed versions details:" + for version in "${failed_versions[@]}"; do + echo "" + echo "--- Go $version (FAILED) ---" + local result_file="${OUTPUT_DIR}/go-${version}.txt" + if [[ -f "$result_file" ]]; then + tail -n 30 "$result_file" + fi + done + fi + } > "$summary_file" + + # Find lowest continuous supported version and check recent versions + local lowest_continuous_version="" + local recent_versions_failed=false + + # Sort versions to ensure proper order + local sorted_versions=() + for version in "${GO_VERSIONS[@]}"; do + sorted_versions+=("$version") + done + # Sort versions numerically (1.11, 1.12, ..., 1.25) + IFS=$'\n' sorted_versions=($(sort -V <<< "${sorted_versions[*]}")) + + # Find lowest continuous supported version (all versions from this point onwards pass) + for version in "${sorted_versions[@]}"; do + local status_file="${OUTPUT_DIR}/go-${version}.txt.status" + local all_subsequent_pass=true + + # Check if this version and all subsequent versions pass + local found_current=false + for check_version in "${sorted_versions[@]}"; do + if [[ "$check_version" == "$version" ]]; then + found_current=true + fi + + if [[ "$found_current" == true ]]; then + local check_status_file="${OUTPUT_DIR}/go-${check_version}.txt.status" + if [[ -f "$check_status_file" ]]; then + local status + status=$(cat "$check_status_file") + if [[ "$status" != "PASSED" ]]; then + all_subsequent_pass=false + break + fi + else + all_subsequent_pass=false + break + fi + fi + done + + if [[ "$all_subsequent_pass" == true ]]; then + lowest_continuous_version="$version" + break + fi + done + + # Check if the two most recent versions failed + local num_versions=${#sorted_versions[@]} + if [[ $num_versions -ge 2 ]]; then + local second_recent="${sorted_versions[$((num_versions-2))]}" + local most_recent="${sorted_versions[$((num_versions-1))]}" + + local second_recent_status_file="${OUTPUT_DIR}/go-${second_recent}.txt.status" + local most_recent_status_file="${OUTPUT_DIR}/go-${most_recent}.txt.status" + + local second_recent_failed=false + local most_recent_failed=false + + if [[ -f "$second_recent_status_file" ]]; then + local status + status=$(cat "$second_recent_status_file") + if [[ "$status" != "PASSED" ]]; then + second_recent_failed=true + fi + else + second_recent_failed=true + fi + + if [[ -f "$most_recent_status_file" ]]; then + local status + status=$(cat "$most_recent_status_file") + if [[ "$status" != "PASSED" ]]; then + most_recent_failed=true + fi + else + most_recent_failed=true + fi + + if [[ "$second_recent_failed" == true || "$most_recent_failed" == true ]]; then + recent_versions_failed=true + fi + elif [[ $num_versions -eq 1 ]]; then + # Only one version tested, check if it's the most recent and failed + local only_version="${sorted_versions[0]}" + local only_status_file="${OUTPUT_DIR}/go-${only_version}.txt.status" + + if [[ -f "$only_status_file" ]]; then + local status + status=$(cat "$only_status_file") + if [[ "$status" != "PASSED" ]]; then + recent_versions_failed=true + fi + else + recent_versions_failed=true + fi + fi + + # Display summary + echo "" + log "Test completed in ${duration}s" + log "Summary report: $summary_file" + + echo "" + echo "========================================" + echo " FINAL RESULTS" + echo "========================================" + echo "" + + # Display passed versions + if [[ ${#passed_versions[@]} -gt 0 ]]; then + log_success "PASSED (${#passed_versions[@]}/${#GO_VERSIONS[@]}):" + # Sort passed versions for display + local sorted_passed=() + for version in "${sorted_versions[@]}"; do + for passed_version in "${passed_versions[@]}"; do + if [[ "$version" == "$passed_version" ]]; then + sorted_passed+=("$version") + break + fi + done + done + for version in "${sorted_passed[@]}"; do + echo -e " ${GREEN}✓${NC} Go $version" + done + echo "" + fi + + # Display failed versions + if [[ ${#failed_versions[@]} -gt 0 ]]; then + log_error "FAILED (${#failed_versions[@]}/${#GO_VERSIONS[@]}):" + # Sort failed versions for display + local sorted_failed=() + for version in "${sorted_versions[@]}"; do + for failed_version in "${failed_versions[@]}"; do + if [[ "$version" == "$failed_version" ]]; then + sorted_failed+=("$version") + break + fi + done + done + for version in "${sorted_failed[@]}"; do + echo -e " ${RED}✗${NC} Go $version" + done + echo "" + + # Show failure details + echo "========================================" + echo " FAILURE DETAILS" + echo "========================================" + echo "" + + for version in "${sorted_failed[@]}"; do + echo -e "${RED}--- Go $version FAILURE LOGS (last 30 lines) ---${NC}" + local result_file="${OUTPUT_DIR}/go-${version}.txt" + if [[ -f "$result_file" ]]; then + tail -n 30 "$result_file" | sed 's/^/ /' + else + echo " No log file found: $result_file" + fi + echo "" + done + fi + + # Display unknown versions + if [[ ${#unknown_versions[@]} -gt 0 ]]; then + log_warning "UNKNOWN (${#unknown_versions[@]}/${#GO_VERSIONS[@]}):" + for version in "${unknown_versions[@]}"; do + echo -e " ${YELLOW}?${NC} Go $version (no status file)" + done + echo "" + fi + + echo "========================================" + echo " COMPATIBILITY SUMMARY" + echo "========================================" + echo "" + + if [[ -n "$lowest_continuous_version" ]]; then + log_success "Lowest continuous supported version: Go $lowest_continuous_version" + echo " (All versions from Go $lowest_continuous_version onwards pass)" + else + log_error "No continuous version support found" + echo " (No version has all subsequent versions passing)" + fi + + echo "" + echo "========================================" + echo "Full detailed logs available in: $OUTPUT_DIR" + echo "========================================" + + # Determine exit code based on recent versions + if [[ "$recent_versions_failed" == true ]]; then + log_error "OVERALL RESULT: Recent Go versions failed - this needs attention!" + if [[ -n "$lowest_continuous_version" ]]; then + echo "Note: Continuous support starts from Go $lowest_continuous_version" + fi + exit 1 + else + log_success "OVERALL RESULT: Recent Go versions pass - compatibility looks good!" + if [[ -n "$lowest_continuous_version" ]]; then + echo "Continuous support starts from Go $lowest_continuous_version" + fi + exit 0 + fi +} + +# Trap to clean up on exit +cleanup() { + # Kill any remaining background processes + jobs -p | xargs -r kill 2>/dev/null || true + + # Clean up any remaining Docker containers + docker ps -q --filter "name=go-toml-test-" | xargs -r docker stop 2>/dev/null || true + docker images -q --filter "reference=go-toml-test-*" | xargs -r docker rmi 2>/dev/null || true +} + +trap cleanup EXIT + +# Run main function +main diff --git a/vendor/github.com/pelletier/go-toml/v2/types.go b/vendor/github.com/pelletier/go-toml/v2/types.go index 3c6b8fe57..6d12fe580 100644 --- a/vendor/github.com/pelletier/go-toml/v2/types.go +++ b/vendor/github.com/pelletier/go-toml/v2/types.go @@ -6,9 +6,18 @@ import ( "time" ) -var timeType = reflect.TypeOf((*time.Time)(nil)).Elem() -var textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() -var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() -var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}(nil)) -var sliceInterfaceType = reflect.TypeOf([]interface{}(nil)) -var stringType = reflect.TypeOf("") +// isZeroer is used to check if a type has a custom IsZero method. +// This allows custom types to define their own zero-value semantics. +type isZeroer interface { + IsZero() bool +} + +var ( + timeType = reflect.TypeOf((*time.Time)(nil)).Elem() + textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() + textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + isZeroerType = reflect.TypeOf((*isZeroer)(nil)).Elem() + mapStringInterfaceType = reflect.TypeOf(map[string]interface{}(nil)) + sliceInterfaceType = reflect.TypeOf([]interface{}(nil)) + stringType = reflect.TypeOf("") +) diff --git a/vendor/github.com/pelletier/go-toml/v2/unmarshaler.go b/vendor/github.com/pelletier/go-toml/v2/unmarshaler.go index 189be525e..e7db8128c 100644 --- a/vendor/github.com/pelletier/go-toml/v2/unmarshaler.go +++ b/vendor/github.com/pelletier/go-toml/v2/unmarshaler.go @@ -12,7 +12,6 @@ import ( "sync/atomic" "time" - "github.com/pelletier/go-toml/v2/internal/danger" "github.com/pelletier/go-toml/v2/internal/tracker" "github.com/pelletier/go-toml/v2/unstable" ) @@ -57,13 +56,18 @@ func (d *Decoder) DisallowUnknownFields() *Decoder { // EnableUnmarshalerInterface allows to enable unmarshaler interface. // -// With this feature enabled, types implementing the unstable/Unmarshaler +// With this feature enabled, types implementing the unstable.Unmarshaler // interface can be decoded from any structure of the document. It allows types // that don't have a straightforward TOML representation to provide their own // decoding logic. // -// Currently, types can only decode from a single value. Tables and array tables -// are not supported. +// The UnmarshalTOML method receives raw TOML bytes: +// - For single values: the raw value bytes (e.g., `"hello"` for a string) +// - For tables: all key-value lines belonging to that table +// - For inline tables/arrays: the raw bytes of the inline structure +// +// The unstable.RawMessage type can be used to capture raw TOML bytes for +// later processing, similar to json.RawMessage. // // *Unstable:* This method does not follow the compatibility guarantees of // semver. It can be changed or removed without a new major version being @@ -123,6 +127,7 @@ func (d *Decoder) Decode(v interface{}) error { dec := decoder{ strict: strict{ Enabled: d.strict, + doc: b, }, unmarshalerInterface: d.unmarshalerInterface, } @@ -226,7 +231,7 @@ func (d *decoder) FromParser(v interface{}) error { } if r.IsNil() { - return fmt.Errorf("toml: decoding pointer target cannot be nil") + return errors.New("toml: decoding pointer target cannot be nil") } r = r.Elem() @@ -273,7 +278,7 @@ func (d *decoder) handleRootExpression(expr *unstable.Node, v reflect.Value) err var err error var first bool // used for to clear array tables on first use - if !(d.skipUntilTable && expr.Kind == unstable.KeyValue) { + if !d.skipUntilTable || expr.Kind != unstable.KeyValue { first, err = d.seen.CheckExpression(expr) if err != nil { return err @@ -378,7 +383,7 @@ func (d *decoder) handleArrayTableCollectionLast(key unstable.Iterator, v reflec case reflect.Array: idx := d.arrayIndex(true, v) if idx >= v.Len() { - return v, fmt.Errorf("%s at position %d", d.typeMismatchError("array table", v.Type()), idx) + return v, fmt.Errorf("%w at position %d", d.typeMismatchError("array table", v.Type()), idx) } elem := v.Index(idx) _, err := d.handleArrayTable(key, elem) @@ -416,27 +421,51 @@ func (d *decoder) handleArrayTableCollection(key unstable.Iterator, v reflect.Va return v, nil case reflect.Slice: - elem := v.Index(v.Len() - 1) + // Create a new element when the slice is empty; otherwise operate on + // the last element. + var ( + elem reflect.Value + created bool + ) + if v.Len() == 0 { + created = true + elemType := v.Type().Elem() + if elemType.Kind() == reflect.Interface { + elem = makeMapStringInterface() + } else { + elem = reflect.New(elemType).Elem() + } + } else { + elem = v.Index(v.Len() - 1) + } + x, err := d.handleArrayTable(key, elem) if err != nil || d.skipUntilTable { return reflect.Value{}, err } if x.IsValid() { - elem.Set(x) + if created { + elem = x + } else { + elem.Set(x) + } } + if created { + return reflect.Append(v, elem), nil + } return v, err case reflect.Array: idx := d.arrayIndex(false, v) if idx >= v.Len() { - return v, fmt.Errorf("%s at position %d", d.typeMismatchError("array table", v.Type()), idx) + return v, fmt.Errorf("%w at position %d", d.typeMismatchError("array table", v.Type()), idx) } elem := v.Index(idx) _, err := d.handleArrayTable(key, elem) return v, err + default: + return d.handleArrayTable(key, v) } - - return d.handleArrayTable(key, v) } func (d *decoder) handleKeyPart(key unstable.Iterator, v reflect.Value, nextFn handlerFn, makeFn valueMakerFn) (reflect.Value, error) { @@ -470,7 +499,8 @@ func (d *decoder) handleKeyPart(key unstable.Iterator, v reflect.Value, nextFn h mv := v.MapIndex(mk) set := false - if !mv.IsValid() { + switch { + case !mv.IsValid(): // If there is no value in the map, create a new one according to // the map type. If the element type is interface, create either a // map[string]interface{} or a []interface{} depending on whether @@ -483,13 +513,13 @@ func (d *decoder) handleKeyPart(key unstable.Iterator, v reflect.Value, nextFn h mv = reflect.New(t).Elem() } set = true - } else if mv.Kind() == reflect.Interface { + case mv.Kind() == reflect.Interface: mv = mv.Elem() if !mv.IsValid() { mv = makeFn() } set = true - } else if !mv.CanAddr() { + case !mv.CanAddr(): vt := v.Type() t := vt.Elem() oldmv := mv @@ -574,18 +604,28 @@ func (d *decoder) handleArrayTablePart(key unstable.Iterator, v reflect.Value) ( // cannot handle it. func (d *decoder) handleTable(key unstable.Iterator, v reflect.Value) (reflect.Value, error) { if v.Kind() == reflect.Slice { - if v.Len() == 0 { - return reflect.Value{}, unstable.NewParserError(key.Node().Data, "cannot store a table in a slice") - } - elem := v.Index(v.Len() - 1) - x, err := d.handleTable(key, elem) - if err != nil { - return reflect.Value{}, err + // For non-empty slices, work with the last element + if v.Len() > 0 { + elem := v.Index(v.Len() - 1) + x, err := d.handleTable(key, elem) + if err != nil { + return reflect.Value{}, err + } + if x.IsValid() { + elem.Set(x) + } + return reflect.Value{}, nil } - if x.IsValid() { - elem.Set(x) + // Empty slice - check if it implements Unmarshaler (e.g., RawMessage) + // and we're at the end of the key path + if d.unmarshalerInterface && !key.Next() { + if v.CanAddr() && v.Addr().CanInterface() { + if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok { + return d.handleKeyValuesUnmarshaler(outi) + } + } } - return reflect.Value{}, nil + return reflect.Value{}, unstable.NewParserError(key.Node().Data, "cannot store a table in a slice") } if key.Next() { // Still scoping the key @@ -599,6 +639,24 @@ func (d *decoder) handleTable(key unstable.Iterator, v reflect.Value) (reflect.V // Handle root expressions until the end of the document or the next // non-key-value. func (d *decoder) handleKeyValues(v reflect.Value) (reflect.Value, error) { + // Check if target implements Unmarshaler before processing key-values. + // This allows types to handle entire tables themselves. + if d.unmarshalerInterface { + vv := v + for vv.Kind() == reflect.Ptr { + if vv.IsNil() { + vv.Set(reflect.New(vv.Type().Elem())) + } + vv = vv.Elem() + } + if vv.CanAddr() && vv.Addr().CanInterface() { + if outi, ok := vv.Addr().Interface().(unstable.Unmarshaler); ok { + // Collect all key-value expressions for this table + return d.handleKeyValuesUnmarshaler(outi) + } + } + } + var rv reflect.Value for d.nextExpr() { expr := d.expr() @@ -628,6 +686,41 @@ func (d *decoder) handleKeyValues(v reflect.Value) (reflect.Value, error) { return rv, nil } +// handleKeyValuesUnmarshaler collects all key-value expressions for a table +// and passes them to the Unmarshaler as raw TOML bytes. +func (d *decoder) handleKeyValuesUnmarshaler(u unstable.Unmarshaler) (reflect.Value, error) { + // Collect raw bytes from all key-value expressions for this table. + // We use the Raw field on each KeyValue expression to preserve the + // original formatting (whitespace, quoting style, etc.) from the document. + var buf []byte + + for d.nextExpr() { + expr := d.expr() + if expr.Kind != unstable.KeyValue { + d.stashExpr() + break + } + + _, err := d.seen.CheckExpression(expr) + if err != nil { + return reflect.Value{}, err + } + + // Use the raw bytes from the original document to preserve formatting + if expr.Raw.Length > 0 { + raw := d.p.Raw(expr.Raw) + buf = append(buf, raw...) + } + buf = append(buf, '\n') + } + + if err := u.UnmarshalTOML(buf); err != nil { + return reflect.Value{}, err + } + + return reflect.Value{}, nil +} + type ( handlerFn func(key unstable.Iterator, v reflect.Value) (reflect.Value, error) valueMakerFn func() reflect.Value @@ -672,14 +765,21 @@ func (d *decoder) handleValue(value *unstable.Node, v reflect.Value) error { if d.unmarshalerInterface { if v.CanAddr() && v.Addr().CanInterface() { if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok { - return outi.UnmarshalTOML(value) + // Pass raw bytes from the original document + return outi.UnmarshalTOML(d.p.Raw(value.Raw)) } } } - ok, err := d.tryTextUnmarshaler(value, v) - if ok || err != nil { - return err + // Only try TextUnmarshaler for scalar types. For Array and InlineTable, + // fall through to struct/map unmarshaling to allow flexible unmarshaling + // where a type can implement UnmarshalText for string values but still + // be populated field-by-field from a table. See issue #974. + if value.Kind != unstable.Array && value.Kind != unstable.InlineTable { + ok, err := d.tryTextUnmarshaler(value, v) + if ok || err != nil { + return err + } } switch value.Kind { @@ -821,6 +921,9 @@ func (d *decoder) unmarshalDateTime(value *unstable.Node, v reflect.Value) error return err } + if v.Kind() != reflect.Interface && v.Type() != timeType { + return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("datetime", v.Type())) + } v.Set(reflect.ValueOf(dt)) return nil } @@ -831,14 +934,14 @@ func (d *decoder) unmarshalLocalDate(value *unstable.Node, v reflect.Value) erro return err } + if v.Kind() != reflect.Interface && v.Type() != timeType { + return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("local date", v.Type())) + } if v.Type() == timeType { - cast := ld.AsTime(time.Local) - v.Set(reflect.ValueOf(cast)) + v.Set(reflect.ValueOf(ld.AsTime(time.Local))) return nil } - v.Set(reflect.ValueOf(ld)) - return nil } @@ -852,6 +955,9 @@ func (d *decoder) unmarshalLocalTime(value *unstable.Node, v reflect.Value) erro return unstable.NewParserError(rest, "extra characters at the end of a local time") } + if v.Kind() != reflect.Interface { + return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("local time", v.Type())) + } v.Set(reflect.ValueOf(lt)) return nil } @@ -866,15 +972,14 @@ func (d *decoder) unmarshalLocalDateTime(value *unstable.Node, v reflect.Value) return unstable.NewParserError(rest, "extra characters at the end of a local date time") } + if v.Kind() != reflect.Interface && v.Type() != timeType { + return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("local datetime", v.Type())) + } if v.Type() == timeType { - cast := ldt.AsTime(time.Local) - - v.Set(reflect.ValueOf(cast)) + v.Set(reflect.ValueOf(ldt.AsTime(time.Local))) return nil } - v.Set(reflect.ValueOf(ldt)) - return nil } @@ -929,8 +1034,9 @@ const ( // compile time, so it is computed during initialization. var maxUint int64 = math.MaxInt64 -func init() { +func init() { //nolint:gochecknoinits m := uint64(^uint(0)) + // #nosec G115 if m < uint64(maxUint) { maxUint = int64(m) } @@ -1010,7 +1116,7 @@ func (d *decoder) unmarshalInteger(value *unstable.Node, v reflect.Value) error case reflect.Interface: r = reflect.ValueOf(i) default: - return unstable.NewParserError(d.p.Raw(value.Raw), d.typeMismatchString("integer", v.Type())) + return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("integer", v.Type())) } if !r.Type().AssignableTo(v.Type()) { @@ -1029,7 +1135,7 @@ func (d *decoder) unmarshalString(value *unstable.Node, v reflect.Value) error { case reflect.Interface: v.Set(reflect.ValueOf(string(value.Data))) default: - return unstable.NewParserError(d.p.Raw(value.Raw), d.typeMismatchString("string", v.Type())) + return unstable.NewParserError(d.p.Raw(value.Raw), "%s", d.typeMismatchString("string", v.Type())) } return nil @@ -1080,35 +1186,39 @@ func (d *decoder) keyFromData(keyType reflect.Type, data []byte) (reflect.Value, return reflect.Value{}, fmt.Errorf("toml: error unmarshalling key type %s from text: %w", stringType, err) } return mk.Elem(), nil + } - case keyType.Kind() == reflect.Int || keyType.Kind() == reflect.Int8 || keyType.Kind() == reflect.Int16 || keyType.Kind() == reflect.Int32 || keyType.Kind() == reflect.Int64: + switch keyType.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: key, err := strconv.ParseInt(string(data), 10, 64) if err != nil { return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from integer: %w", stringType, err) } return reflect.ValueOf(key).Convert(keyType), nil - case keyType.Kind() == reflect.Uint || keyType.Kind() == reflect.Uint8 || keyType.Kind() == reflect.Uint16 || keyType.Kind() == reflect.Uint32 || keyType.Kind() == reflect.Uint64: + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: key, err := strconv.ParseUint(string(data), 10, 64) if err != nil { return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from unsigned integer: %w", stringType, err) } return reflect.ValueOf(key).Convert(keyType), nil - case keyType.Kind() == reflect.Float32: + case reflect.Float32: key, err := strconv.ParseFloat(string(data), 32) if err != nil { return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from float: %w", stringType, err) } return reflect.ValueOf(float32(key)), nil - case keyType.Kind() == reflect.Float64: + case reflect.Float64: key, err := strconv.ParseFloat(string(data), 64) if err != nil { return reflect.Value{}, fmt.Errorf("toml: error parsing key of type %s from float: %w", stringType, err) } return reflect.ValueOf(float64(key)), nil + + default: + return reflect.Value{}, fmt.Errorf("toml: cannot convert map key of type %s to expected type %s", stringType, keyType) } - return reflect.Value{}, fmt.Errorf("toml: cannot convert map key of type %s to expected type %s", stringType, keyType) } func (d *decoder) handleKeyValuePart(key unstable.Iterator, value *unstable.Node, v reflect.Value) (reflect.Value, error) { @@ -1154,6 +1264,18 @@ func (d *decoder) handleKeyValuePart(key unstable.Iterator, value *unstable.Node case reflect.Struct: path, found := structFieldPath(v, string(key.Node().Data)) if !found { + // If no matching struct field is found but the target implements the + // unstable.Unmarshaler interface (and it is enabled), delegate the + // decoding of this value to the custom unmarshaler. + if d.unmarshalerInterface { + if v.CanAddr() && v.Addr().CanInterface() { + if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok { + // Pass raw bytes from the original document + return reflect.Value{}, outi.UnmarshalTOML(d.p.Raw(value.Raw)) + } + } + } + // Otherwise, keep previous behavior and skip until the next table. d.skipUntilTable = true break } @@ -1259,13 +1381,13 @@ func fieldByIndex(v reflect.Value, path []int) reflect.Value { type fieldPathsMap = map[string][]int -var globalFieldPathsCache atomic.Value // map[danger.TypeID]fieldPathsMap +var globalFieldPathsCache atomic.Value // map[reflect.Type]fieldPathsMap func structFieldPath(v reflect.Value, name string) ([]int, bool) { t := v.Type() - cache, _ := globalFieldPathsCache.Load().(map[danger.TypeID]fieldPathsMap) - fieldPaths, ok := cache[danger.MakeTypeID(t)] + cache, _ := globalFieldPathsCache.Load().(map[reflect.Type]fieldPathsMap) + fieldPaths, ok := cache[t] if !ok { fieldPaths = map[string][]int{} @@ -1276,8 +1398,8 @@ func structFieldPath(v reflect.Value, name string) ([]int, bool) { fieldPaths[strings.ToLower(name)] = path }) - newCache := make(map[danger.TypeID]fieldPathsMap, len(cache)+1) - newCache[danger.MakeTypeID(t)] = fieldPaths + newCache := make(map[reflect.Type]fieldPathsMap, len(cache)+1) + newCache[t] = fieldPaths for k, v := range cache { newCache[k] = v } @@ -1301,7 +1423,9 @@ func forEachField(t reflect.Type, path []int, do func(name string, path []int)) continue } - fieldPath := append(path, i) + fieldPath := make([]int, 0, len(path)+1) + fieldPath = append(fieldPath, path...) + fieldPath = append(fieldPath, i) fieldPath = fieldPath[:len(fieldPath):len(fieldPath)] name := f.Tag.Get("toml") diff --git a/vendor/github.com/pelletier/go-toml/v2/unstable/ast.go b/vendor/github.com/pelletier/go-toml/v2/unstable/ast.go index f526bf2c0..6b21592d6 100644 --- a/vendor/github.com/pelletier/go-toml/v2/unstable/ast.go +++ b/vendor/github.com/pelletier/go-toml/v2/unstable/ast.go @@ -1,10 +1,8 @@ package unstable import ( + "errors" "fmt" - "unsafe" - - "github.com/pelletier/go-toml/v2/internal/danger" ) // Iterator over a sequence of nodes. @@ -19,30 +17,43 @@ import ( // // do something with n // } type Iterator struct { + nodes *[]Node + idx int32 started bool - node *Node } // Next moves the iterator forward and returns true if points to a // node, false otherwise. func (c *Iterator) Next() bool { + if c.nodes == nil { + return false + } + nodes := *c.nodes if !c.started { c.started = true - } else if c.node.Valid() { - c.node = c.node.Next() + } else { + idx := c.idx + if idx >= 0 && int(idx) < len(nodes) { + c.idx = nodes[idx].next + } } - return c.node.Valid() + return c.idx >= 0 && int(c.idx) < len(nodes) } // IsLast returns true if the current node of the iterator is the last // one. Subsequent calls to Next() will return false. func (c *Iterator) IsLast() bool { - return c.node.next == 0 + return c.nodes == nil || c.idx < 0 || (*c.nodes)[c.idx].next < 0 } // Node returns a pointer to the node pointed at by the iterator. func (c *Iterator) Node() *Node { - return c.node + if c.nodes == nil || c.idx < 0 { + return nil + } + n := &(*c.nodes)[c.idx] + n.nodes = c.nodes + return n } // Node in a TOML expression AST. @@ -65,11 +76,12 @@ type Node struct { Raw Range // Raw bytes from the input. Data []byte // Node value (either allocated or referencing the input). - // References to other nodes, as offsets in the backing array - // from this node. References can go backward, so those can be - // negative. - next int // 0 if last element - child int // 0 if no child + // Absolute indices into the backing nodes slice. -1 means none. + next int32 + child int32 + + // Reference to the backing nodes slice for navigation. + nodes *[]Node } // Range of bytes in the document. @@ -80,24 +92,24 @@ type Range struct { // Next returns a pointer to the next node, or nil if there is no next node. func (n *Node) Next() *Node { - if n.next == 0 { + if n.next < 0 { return nil } - ptr := unsafe.Pointer(n) - size := unsafe.Sizeof(Node{}) - return (*Node)(danger.Stride(ptr, size, n.next)) + next := &(*n.nodes)[n.next] + next.nodes = n.nodes + return next } // Child returns a pointer to the first child node of this node. Other children -// can be accessed calling Next on the first child. Returns an nil if this Node +// can be accessed calling Next on the first child. Returns nil if this Node // has no child. func (n *Node) Child() *Node { - if n.child == 0 { + if n.child < 0 { return nil } - ptr := unsafe.Pointer(n) - size := unsafe.Sizeof(Node{}) - return (*Node)(danger.Stride(ptr, size, n.child)) + child := &(*n.nodes)[n.child] + child.nodes = n.nodes + return child } // Valid returns true if the node's kind is set (not to Invalid). @@ -111,13 +123,14 @@ func (n *Node) Valid() bool { func (n *Node) Key() Iterator { switch n.Kind { case KeyValue: - value := n.Child() - if !value.Valid() { - panic(fmt.Errorf("KeyValue should have at least two children")) + child := n.child + if child < 0 { + panic(errors.New("KeyValue should have at least two children")) } - return Iterator{node: value.Next()} + valueNode := &(*n.nodes)[child] + return Iterator{nodes: n.nodes, idx: valueNode.next} case Table, ArrayTable: - return Iterator{node: n.Child()} + return Iterator{nodes: n.nodes, idx: n.child} default: panic(fmt.Errorf("Key() is not supported on a %s", n.Kind)) } @@ -132,5 +145,5 @@ func (n *Node) Value() *Node { // Children returns an iterator over a node's children. func (n *Node) Children() Iterator { - return Iterator{node: n.Child()} + return Iterator{nodes: n.nodes, idx: n.child} } diff --git a/vendor/github.com/pelletier/go-toml/v2/unstable/builder.go b/vendor/github.com/pelletier/go-toml/v2/unstable/builder.go index 9538e30df..e4354985b 100644 --- a/vendor/github.com/pelletier/go-toml/v2/unstable/builder.go +++ b/vendor/github.com/pelletier/go-toml/v2/unstable/builder.go @@ -7,15 +7,6 @@ type root struct { nodes []Node } -// Iterator over the top level nodes. -func (r *root) Iterator() Iterator { - it := Iterator{} - if len(r.nodes) > 0 { - it.node = &r.nodes[0] - } - return it -} - func (r *root) at(idx reference) *Node { return &r.nodes[idx] } @@ -33,12 +24,10 @@ type builder struct { lastIdx int } -func (b *builder) Tree() *root { - return &b.tree -} - func (b *builder) NodeAt(ref reference) *Node { - return b.tree.at(ref) + n := b.tree.at(ref) + n.nodes = &b.tree.nodes + return n } func (b *builder) Reset() { @@ -48,24 +37,28 @@ func (b *builder) Reset() { func (b *builder) Push(n Node) reference { b.lastIdx = len(b.tree.nodes) + n.next = -1 + n.child = -1 b.tree.nodes = append(b.tree.nodes, n) return reference(b.lastIdx) } func (b *builder) PushAndChain(n Node) reference { newIdx := len(b.tree.nodes) + n.next = -1 + n.child = -1 b.tree.nodes = append(b.tree.nodes, n) if b.lastIdx >= 0 { - b.tree.nodes[b.lastIdx].next = newIdx - b.lastIdx + b.tree.nodes[b.lastIdx].next = int32(newIdx) //nolint:gosec // TOML ASTs are small } b.lastIdx = newIdx return reference(b.lastIdx) } func (b *builder) AttachChild(parent reference, child reference) { - b.tree.nodes[parent].child = int(child) - int(parent) + b.tree.nodes[parent].child = int32(child) //nolint:gosec // TOML ASTs are small } func (b *builder) Chain(from reference, to reference) { - b.tree.nodes[from].next = int(to) - int(from) + b.tree.nodes[from].next = int32(to) //nolint:gosec // TOML ASTs are small } diff --git a/vendor/github.com/pelletier/go-toml/v2/unstable/kind.go b/vendor/github.com/pelletier/go-toml/v2/unstable/kind.go index ff9df1bef..f87a95a78 100644 --- a/vendor/github.com/pelletier/go-toml/v2/unstable/kind.go +++ b/vendor/github.com/pelletier/go-toml/v2/unstable/kind.go @@ -6,28 +6,40 @@ import "fmt" type Kind int const ( - // Meta + // Invalid represents an invalid meta node. Invalid Kind = iota + // Comment represents a comment meta node. Comment + // Key represents a key meta node. Key - // Top level structures + // Table represents a top-level table. Table + // ArrayTable represents a top-level array table. ArrayTable + // KeyValue represents a top-level key value. KeyValue - // Containers values + // Array represents an array container value. Array + // InlineTable represents an inline table container value. InlineTable - // Values + // String represents a string value. String + // Bool represents a boolean value. Bool + // Float represents a floating point value. Float + // Integer represents an integer value. Integer + // LocalDate represents a a local date value. LocalDate + // LocalTime represents a local time value. LocalTime + // LocalDateTime represents a local date/time value. LocalDateTime + // DateTime represents a data/time value. DateTime ) diff --git a/vendor/github.com/pelletier/go-toml/v2/unstable/parser.go b/vendor/github.com/pelletier/go-toml/v2/unstable/parser.go index 50358a44f..e7c68dc5c 100644 --- a/vendor/github.com/pelletier/go-toml/v2/unstable/parser.go +++ b/vendor/github.com/pelletier/go-toml/v2/unstable/parser.go @@ -6,7 +6,6 @@ import ( "unicode" "github.com/pelletier/go-toml/v2/internal/characters" - "github.com/pelletier/go-toml/v2/internal/danger" ) // ParserError describes an error relative to the content of the document. @@ -70,11 +69,26 @@ func (p *Parser) Data() []byte { // panics. func (p *Parser) Range(b []byte) Range { return Range{ - Offset: uint32(danger.SubsliceOffset(p.data, b)), - Length: uint32(len(b)), + Offset: uint32(p.subsliceOffset(b)), //nolint:gosec // TOML documents are small + Length: uint32(len(b)), //nolint:gosec // TOML documents are small } } +// rangeOfToken computes the Range of a token given the remaining bytes after the token. +// This is used when the token was extracted from the beginning of some position, +// and 'rest' is what remains after the token. +func (p *Parser) rangeOfToken(token, rest []byte) Range { + offset := len(p.data) - len(token) - len(rest) + return Range{Offset: uint32(offset), Length: uint32(len(token))} //nolint:gosec // TOML documents are small +} + +// subsliceOffset returns the byte offset of subslice b within p.data. +// b must be a suffix (tail) of p.data. +func (p *Parser) subsliceOffset(b []byte) int { + // b is a suffix of p.data, so its offset is len(p.data) - len(b) + return len(p.data) - len(b) +} + // Raw returns the slice corresponding to the bytes in the given range. func (p *Parser) Raw(raw Range) []byte { return p.data[raw.Offset : raw.Offset+raw.Length] @@ -158,9 +172,17 @@ type Shape struct { End Position } -func (p *Parser) position(b []byte) Position { - offset := danger.SubsliceOffset(p.data, b) +// Shape returns the shape of the given range in the input. Will +// panic if the range is not a subslice of the input. +func (p *Parser) Shape(r Range) Shape { + return Shape{ + Start: p.positionAt(int(r.Offset)), + End: p.positionAt(int(r.Offset + r.Length)), + } +} +// positionAt returns the position at the given byte offset in the document. +func (p *Parser) positionAt(offset int) Position { lead := p.data[:offset] return Position{ @@ -170,16 +192,6 @@ func (p *Parser) position(b []byte) Position { } } -// Shape returns the shape of the given range in the input. Will -// panic if the range is not a subslice of the input. -func (p *Parser) Shape(r Range) Shape { - raw := p.Raw(r) - return Shape{ - Start: p.position(raw), - End: p.position(raw[r.Length:]), - } -} - func (p *Parser) parseNewline(b []byte) ([]byte, error) { if b[0] == '\n' { return b[1:], nil @@ -199,7 +211,7 @@ func (p *Parser) parseComment(b []byte) (reference, []byte, error) { if p.KeepComments && err == nil { ref = p.builder.Push(Node{ Kind: Comment, - Raw: p.Range(data), + Raw: p.rangeOfToken(data, rest), Data: data, }) } @@ -316,6 +328,9 @@ func (p *Parser) parseStdTable(b []byte) (reference, []byte, error) { func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) { // keyval = key keyval-sep val + // Track the start position for Raw range + startB := b + ref := p.builder.Push(Node{ Kind: KeyValue, }) @@ -330,7 +345,7 @@ func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) { b = p.parseWhitespace(b) if len(b) == 0 { - return invalidReference, nil, NewParserError(b, "expected = after a key, but the document ends there") + return invalidReference, nil, NewParserError(startB[:len(startB)-len(b)], "expected = after a key, but the document ends there") } b, err = expect('=', b) @@ -348,6 +363,11 @@ func (p *Parser) parseKeyval(b []byte) (reference, []byte, error) { p.builder.Chain(valRef, key) p.builder.AttachChild(ref, valRef) + // Set Raw to span the entire key-value expression. + // Access the node directly in the slice to avoid the write barrier + // that NodeAt's nodes-pointer setup would trigger. + p.builder.tree.nodes[ref].Raw = p.rangeOfToken(startB[:len(startB)-len(b)], b) + return ref, b, err } @@ -376,7 +396,7 @@ func (p *Parser) parseVal(b []byte) (reference, []byte, error) { if err == nil { ref = p.builder.Push(Node{ Kind: String, - Raw: p.Range(raw), + Raw: p.rangeOfToken(raw, b), Data: v, }) } @@ -394,7 +414,7 @@ func (p *Parser) parseVal(b []byte) (reference, []byte, error) { if err == nil { ref = p.builder.Push(Node{ Kind: String, - Raw: p.Range(raw), + Raw: p.rangeOfToken(raw, b), Data: v, }) } @@ -456,7 +476,7 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) { // inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ] parent := p.builder.Push(Node{ Kind: InlineTable, - Raw: p.Range(b[:1]), + Raw: p.rangeOfToken(b[:1], b[1:]), }) first := true @@ -542,7 +562,7 @@ func (p *Parser) parseValArray(b []byte) (reference, []byte, error) { var err error for len(b) > 0 { - cref := invalidReference + var cref reference cref, b, err = p.parseOptionalWhitespaceCommentNewline(b) if err != nil { return parent, nil, err @@ -611,12 +631,13 @@ func (p *Parser) parseOptionalWhitespaceCommentNewline(b []byte) (reference, []b latestCommentRef := invalidReference addComment := func(ref reference) { - if rootCommentRef == invalidReference { + switch { + case rootCommentRef == invalidReference: rootCommentRef = ref - } else if latestCommentRef == invalidReference { + case latestCommentRef == invalidReference: p.builder.AttachChild(rootCommentRef, ref) latestCommentRef = ref - } else { + default: p.builder.Chain(latestCommentRef, ref) latestCommentRef = ref } @@ -704,11 +725,11 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er if !escaped { str := token[startIdx:endIdx] - verr := characters.Utf8TomlValidAlreadyEscaped(str) - if verr.Zero() { + highlight := characters.Utf8TomlValidAlreadyEscaped(str) + if len(highlight) == 0 { return token, str, rest, nil } - return nil, nil, nil, NewParserError(str[verr.Index:verr.Index+verr.Size], "invalid UTF-8") + return nil, nil, nil, NewParserError(highlight, "invalid UTF-8") } var builder bytes.Buffer @@ -744,7 +765,7 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er i += j for ; i < len(token)-3; i++ { c := token[i] - if !(c == '\n' || c == '\r' || c == ' ' || c == '\t') { + if c != '\n' && c != '\r' && c != ' ' && c != '\t' { i-- break } @@ -820,7 +841,7 @@ func (p *Parser) parseKey(b []byte) (reference, []byte, error) { ref := p.builder.Push(Node{ Kind: Key, - Raw: p.Range(raw), + Raw: p.rangeOfToken(raw, b), Data: key, }) @@ -836,7 +857,7 @@ func (p *Parser) parseKey(b []byte) (reference, []byte, error) { p.builder.PushAndChain(Node{ Kind: Key, - Raw: p.Range(raw), + Raw: p.rangeOfToken(raw, b), Data: key, }) } else { @@ -897,11 +918,11 @@ func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) { // validate the string and return a direct reference to the buffer. if !escaped { str := token[startIdx:endIdx] - verr := characters.Utf8TomlValidAlreadyEscaped(str) - if verr.Zero() { + highlight := characters.Utf8TomlValidAlreadyEscaped(str) + if len(highlight) == 0 { return token, str, rest, nil } - return nil, nil, nil, NewParserError(str[verr.Index:verr.Index+verr.Size], "invalid UTF-8") + return nil, nil, nil, NewParserError(highlight, "invalid UTF-8") } i := startIdx @@ -972,7 +993,7 @@ func hexToRune(b []byte, length int) (rune, error) { var r uint32 for i, c := range b { - d := uint32(0) + var d uint32 switch { case '0' <= c && c <= '9': d = uint32(c - '0') @@ -1013,7 +1034,7 @@ func (p *Parser) parseIntOrFloatOrDateTime(b []byte) (reference, []byte, error) return p.builder.Push(Node{ Kind: Float, Data: b[:3], - Raw: p.Range(b[:3]), + Raw: p.rangeOfToken(b[:3], b[3:]), }), b[3:], nil case 'n': if !scanFollowsNan(b) { @@ -1023,7 +1044,7 @@ func (p *Parser) parseIntOrFloatOrDateTime(b []byte) (reference, []byte, error) return p.builder.Push(Node{ Kind: Float, Data: b[:3], - Raw: p.Range(b[:3]), + Raw: p.rangeOfToken(b[:3], b[3:]), }), b[3:], nil case '+', '-': return p.scanIntOrFloat(b) @@ -1076,7 +1097,7 @@ byteLoop: } case c == 'T' || c == 't' || c == ':' || c == '.': hasTime = true - case c == '+' || c == '-' || c == 'Z' || c == 'z': + case c == '+' || c == 'Z' || c == 'z': hasTz = true case c == ' ': if !seenSpace && i+1 < len(b) && isDigit(b[i+1]) { @@ -1148,7 +1169,7 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) { return p.builder.Push(Node{ Kind: Integer, Data: b[:i], - Raw: p.Range(b[:i]), + Raw: p.rangeOfToken(b[:i], b[i:]), }), b[i:], nil } @@ -1172,7 +1193,7 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) { return p.builder.Push(Node{ Kind: Float, Data: b[:i+3], - Raw: p.Range(b[:i+3]), + Raw: p.rangeOfToken(b[:i+3], b[i+3:]), }), b[i+3:], nil } @@ -1184,7 +1205,7 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) { return p.builder.Push(Node{ Kind: Float, Data: b[:i+3], - Raw: p.Range(b[:i+3]), + Raw: p.rangeOfToken(b[:i+3], b[i+3:]), }), b[i+3:], nil } @@ -1207,7 +1228,7 @@ func (p *Parser) scanIntOrFloat(b []byte) (reference, []byte, error) { return p.builder.Push(Node{ Kind: kind, Data: b[:i], - Raw: p.Range(b[:i]), + Raw: p.rangeOfToken(b[:i], b[i:]), }), b[i:], nil } diff --git a/vendor/github.com/pelletier/go-toml/v2/unstable/unmarshaler.go b/vendor/github.com/pelletier/go-toml/v2/unstable/unmarshaler.go index 00cfd6de4..5a79da88e 100644 --- a/vendor/github.com/pelletier/go-toml/v2/unstable/unmarshaler.go +++ b/vendor/github.com/pelletier/go-toml/v2/unstable/unmarshaler.go @@ -1,7 +1,32 @@ package unstable -// The Unmarshaler interface may be implemented by types to customize their -// behavior when being unmarshaled from a TOML document. +// Unmarshaler is implemented by types that can unmarshal a TOML +// description of themselves. The input is a valid TOML document +// containing the relevant portion of the parsed document. +// +// For tables (including split tables defined in multiple places), +// the data contains the raw key-value bytes from the original document +// with adjusted table headers to be relative to the unmarshaling target. type Unmarshaler interface { - UnmarshalTOML(value *Node) error + UnmarshalTOML(data []byte) error +} + +// RawMessage is a raw encoded TOML value. It implements Unmarshaler +// and can be used to delay TOML decoding or capture raw content. +// +// Example usage: +// +// type Config struct { +// Plugin RawMessage `toml:"plugin"` +// } +// +// var cfg Config +// toml.NewDecoder(r).EnableUnmarshalerInterface().Decode(&cfg) +// // cfg.Plugin now contains the raw TOML bytes for [plugin] +type RawMessage []byte + +// UnmarshalTOML implements Unmarshaler. +func (m *RawMessage) UnmarshalTOML(data []byte) error { + *m = append((*m)[0:0], data...) + return nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 41338b223..ed5cda215 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -275,11 +275,10 @@ github.com/opencontainers/go-digest ## explicit; go 1.18 github.com/opencontainers/image-spec/specs-go github.com/opencontainers/image-spec/specs-go/v1 -# github.com/pelletier/go-toml/v2 v2.2.4 +# github.com/pelletier/go-toml/v2 v2.3.0 ## explicit; go 1.21.0 github.com/pelletier/go-toml/v2 github.com/pelletier/go-toml/v2/internal/characters -github.com/pelletier/go-toml/v2/internal/danger github.com/pelletier/go-toml/v2/internal/tracker github.com/pelletier/go-toml/v2/unstable # github.com/pires/go-proxyproto v0.11.0 @@ -321,7 +320,7 @@ github.com/sirupsen/logrus/hooks/syslog # github.com/skycoin/noise v0.0.0-20180327030543-2492fe189ae6 ## explicit github.com/skycoin/noise -# github.com/skycoin/skycoin v0.28.5-alpha1.0.20260323015226-90b668188f85 +# github.com/skycoin/skycoin v0.28.6-0.20260325014814-f48988877c68 ## explicit; go 1.26.1 github.com/skycoin/skycoin/src/cipher github.com/skycoin/skycoin/src/cipher/base58