commit 84bb5c194609746ed5af41cc3f3595efb8be902f Author: Luke Street Date: Fri Aug 16 00:41:38 2024 -0600 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2277617 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/tmp +/.idea +objdiff-* +config.yml diff --git a/cmd/decompal/main.go b/cmd/decompal/main.go new file mode 100644 index 0000000..df6bed9 --- /dev/null +++ b/cmd/decompal/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "github.com/encounter/decompal/config" + "github.com/encounter/decompal/handlers" + "github.com/gregjones/httpcache" + "github.com/palantir/go-baseapp/baseapp" + "github.com/palantir/go-githubapp/githubapp" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "goji.io/pat" + "time" +) + +func main() { + // Load configuration from a file + cfg, err := config.ReadConfig("config.yml") + if err != nil { + panic(errors.Wrap(err, "failed to read config")) + } + logger := baseapp.NewLogger(cfg.Logging) + zerolog.DefaultContextLogger = &logger + + // Configure a temporary directory + //if cfg.App.TmpDir == "" { + // cfg.App.TmpDir, err = os.MkdirTemp(os.TempDir(), "decompal") + // if err != nil { + // logger.Fatal(). + // Err(err). + // Str("path", cfg.App.TmpDir). + // Msg("failed to create temporary directory") + // } + //} + //if _, err := os.Stat(cfg.App.TmpDir); err != nil { + // if os.IsNotExist(err) { + // err := os.MkdirAll(cfg.App.TmpDir, 0755) + // if err != nil { + // logger.Fatal(). + // Err(err). + // Str("path", cfg.App.TmpDir). + // Msg("failed to create temporary directory") + // } + // } else { + // logger.Fatal(). + // Err(err). + // Str("path", cfg.App.TmpDir). + // Msg("failed to stat temporary directory") + // } + //} + //logger.Debug(). + // Str("path", cfg.App.TmpDir). + // Msg("Using temporary directory") + //// Delete the temporary directory on exit + //defer func() { + // if err := os.RemoveAll(cfg.App.TmpDir); err != nil { + // logger.Error(). + // Err(err). + // Str("path", cfg.App.TmpDir). + // Msg("failed to remove temporary directory") + // } + //}() + + // Create a task queue + //queueFactory := memqueue.NewFactory() + //taskQueue := queueFactory.RegisterQueue(&taskq.QueueOptions{ + // Name: "background-tasks", + //}) + + // Create the server + serverParams := baseapp.DefaultParams(logger, "decompal.") + server, err := baseapp.NewServer(cfg.Server, serverParams...) + if err != nil { + logger.Fatal().Err(err).Msg("failed to create server") + } + + // Create GitHub app client + cc, err := githubapp.NewDefaultCachingClientCreator( + cfg.GitHub, + githubapp.WithClientUserAgent("decompal/1.0.0"), + githubapp.WithClientTimeout(5*time.Second), + githubapp.WithClientCaching(false, func() httpcache.Cache { return httpcache.NewMemoryCache() }), + ) + if err != nil { + logger.Fatal().Err(err).Msg("failed to create GitHub app client") + } + + // Register GitHub webhook handlers + taskCtx, taskCancel := context.WithCancel(context.Background()) + defer taskCancel() + server.Mux().Handle(pat.Post(githubapp.DefaultWebhookRoute), githubapp.NewDefaultEventDispatcher( + cfg.GitHub, + handlers.NewPullRequestHandler(cc, &cfg.App, taskCtx), + handlers.NewWorkflowRunHandler(cc, &cfg.App, taskCtx), + )) + + // Start the server (blocking) + if err = server.Start(); err != nil { + logger.Fatal().Err(err).Msg("server failed") + } +} diff --git a/config.example.yml b/config.example.yml new file mode 100644 index 0000000..a22c07c --- /dev/null +++ b/config.example.yml @@ -0,0 +1,25 @@ +server: + address: 127.0.0.1 + port: 8080 + shutdown_wait_time: 15s + +logging: + pretty: true + level: debug + +app: + objdiff_path: ./objdiff-cli-linux-x86_64 + +github: + v3_api_url: https://api.github.com/ + v4_api_url: https://api.github.com/graphql + app: + integration_id: 1234 + webhook_secret: webhook_secret + private_key: | + -----BEGIN RSA PRIVATE KEY----- + ... + -----END RSA PRIVATE KEY----- + oauth: + client_id: client_id + client_secret: client_secret diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..5e58228 --- /dev/null +++ b/config/config.go @@ -0,0 +1,36 @@ +package config + +import ( + "github.com/palantir/go-baseapp/baseapp" + "github.com/palantir/go-githubapp/githubapp" + "github.com/pkg/errors" + "gopkg.in/yaml.v3" + "os" +) + +type Config struct { + Server baseapp.HTTPConfig `yaml:"server"` + Logging baseapp.LoggingConfig `yaml:"logging"` + GitHub githubapp.Config `yaml:"github"` + App AppConfig `yaml:"app"` +} + +type AppConfig struct { + //TmpDir string `yaml:"tmp_dir"` + ObjdiffPath string `yaml:"objdiff_path"` +} + +func ReadConfig(path string) (Config, error) { + var c Config + + bytes, err := os.ReadFile(path) + if err != nil { + return c, errors.Wrapf(err, "failed reading server config file: %s", path) + } + + if err := yaml.Unmarshal(bytes, &c); err != nil { + return c, errors.Wrap(err, "failed parsing configuration file") + } + + return c, nil +} diff --git a/genproto.sh b/genproto.sh new file mode 100755 index 0000000..71d4534 --- /dev/null +++ b/genproto.sh @@ -0,0 +1,6 @@ +#!/bin/sh -e +SRC_DIR=../objdiff/objdiff-cli/protos +protoc -I=$SRC_DIR \ + --go_out=paths=source_relative:objdiff \ + --go_opt=Mreport.proto=github.com/encounter/decompal/objdiff \ + $SRC_DIR/*.proto diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a9a3b66 --- /dev/null +++ b/go.mod @@ -0,0 +1,34 @@ +module github.com/encounter/decompal + +go 1.22 + +require ( + github.com/google/go-github/v63 v63.0.0 + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 + github.com/palantir/go-baseapp v0.5.2 + github.com/palantir/go-githubapp v0.27.0 + github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.33.0 + goji.io v2.0.2+incompatible + google.golang.org/protobuf v1.34.2 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/bluekeyes/hatpear v0.1.2 // indirect + github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/google/go-github/v62 v62.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 // indirect + github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect + golang.org/x/oauth2 v0.22.0 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..359b88f --- /dev/null +++ b/go.sum @@ -0,0 +1,75 @@ +github.com/bluekeyes/hatpear v0.1.2 h1:D5dz9W/W5YxKlmqn7hRJi218+yVkXylhs/R6Ns/jj4A= +github.com/bluekeyes/hatpear v0.1.2/go.mod h1:2bh+rl4wLhqzzL0hT7Q4SVGXIivrE8oKgH2WYM3ubt0= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= +github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= +github.com/google/go-github/v63 v63.0.0 h1:13xwK/wk9alSokujB9lJkuzdmQuVn2QCPeck76wR3nE= +github.com/google/go-github/v63 v63.0.0/go.mod h1:IqbcrgUmIcEaioWrGYei/09o+ge5vhffGOcxrO0AfmA= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/palantir/go-baseapp v0.5.2 h1:b1ukx7AXo2/E4NkUvTFlW+185uwCcifzd2XzLrG4oS8= +github.com/palantir/go-baseapp v0.5.2/go.mod h1:uijQMPfmgV69oiMu2jkskum/4HiYuEP/gzrnphD+/Co= +github.com/palantir/go-githubapp v0.27.0 h1:YTSnLElOVOSSFOqCiAyc1wpZMQjGmMvjas0W/F9W4Jk= +github.com/palantir/go-githubapp v0.27.0/go.mod h1:yi9WLdjHkIfeZMetaZyCFJLcxocmrYdd+e5BJTfkimI= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M= +github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= +github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= +github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c= +goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/pull_request.go b/handlers/pull_request.go new file mode 100644 index 0000000..cb6b1bd --- /dev/null +++ b/handlers/pull_request.go @@ -0,0 +1,123 @@ +package handlers + +import ( + "context" + "encoding/json" + "github.com/encounter/decompal/config" + "github.com/encounter/decompal/objdiff" + "github.com/google/go-github/v63/github" + "github.com/palantir/go-githubapp/githubapp" + "github.com/pkg/errors" +) + +type pullRequestHandler struct { + githubapp.ClientCreator + config *config.AppConfig + taskCtx context.Context +} + +func NewPullRequestHandler( + cc githubapp.ClientCreator, + config *config.AppConfig, + taskCtx context.Context, +) githubapp.EventHandler { + return &pullRequestHandler{ + ClientCreator: cc, + config: config, + taskCtx: taskCtx, + } +} + +func (h *pullRequestHandler) Handles() []string { + return []string{"pull_request"} +} + +func (h *pullRequestHandler) Handle(_ context.Context, eventType, deliveryID string, payload []byte) error { + event := &github.PullRequestEvent{} + if err := json.Unmarshal(payload, event); err != nil { + return errors.Wrap(err, "failed to parse pull request event payload") + } + if event.GetAction() != "opened" { + return nil + } + + installationID := githubapp.GetInstallationIDFromEvent(event) + client, err := h.NewInstallationClient(installationID) + if err != nil { + return err + } + + go backgroundTask(h.taskCtx, eventType, deliveryID, func(ctx context.Context) error { + repo := event.GetRepo() + // Re-prepare logger instead of using the one from the request context + ctx, logger := githubapp.PrepareRepoContext(ctx, installationID, repo) + + // Find any completed workflow runs for the current PR + repoOwner := repo.GetOwner().GetLogin() + repoName := repo.GetName() + pr := event.GetPullRequest() + sha := pr.GetHead().GetSHA() + runs, _, err := client.Actions.ListRepositoryWorkflowRuns( + ctx, + repoOwner, + repoName, + &github.ListWorkflowRunsOptions{ + Status: "completed", + HeadSHA: sha, + ExcludePullRequests: true, + }, + ) + if err != nil { + return errors.Wrap(err, "failed to list workflow runs") + } + if len(runs.WorkflowRuns) == 0 { + logger.Debug().Msg("No workflow runs found") + return nil + } + + // Find report files in any completed workflow runs + var files []objdiff.ReportFile + var run *github.WorkflowRun + for _, run = range runs.WorkflowRuns { + files, err = objdiff.FetchReportFiles( + ctx, + logger, + client, + repoOwner, + repoName, + sha, + run.GetID(), + ) + if err != nil { + return err + } + if len(files) > 0 { + break + } + } + if run == nil || len(files) == 0 { + logger.Info().Msg("No report files found") + return nil + } + + // Generate changes and create a PR comment + err = processPR( + ctx, + h.config, + installationID, + pr, + sha, + client, + repo, + run.GetWorkflowID(), + files, + ) + if err != nil { + return err + } + + return nil + }) + + return nil +} diff --git a/handlers/shared.go b/handlers/shared.go new file mode 100644 index 0000000..ba8a6ee --- /dev/null +++ b/handlers/shared.go @@ -0,0 +1,387 @@ +package handlers + +import ( + "context" + "fmt" + "github.com/encounter/decompal/config" + "github.com/encounter/decompal/objdiff" + "github.com/google/go-github/v63/github" + "github.com/palantir/go-githubapp/githubapp" + "github.com/pkg/errors" + "strings" +) + +func processPR( + ctx context.Context, + config *config.AppConfig, + installationID int64, + pr *github.PullRequest, + sha string, + client *github.Client, + repo *github.Repository, + workflowID int64, + files []objdiff.ReportFile, +) error { + prNum := pr.GetNumber() + ctx, logger := githubapp.PreparePRContext(ctx, installationID, repo, prNum) + + // Sanity check + head := pr.GetHead() + if head.GetSHA() != sha { + logger.Debug(). + Str("head_sha", head.GetSHA()). + Str("commit_sha", sha). + Msg("Head SHA does not match workflow run SHA") + return nil + } + + // Find workflows runs for the PR base commit + base := pr.GetBase() + repoOwner := repo.GetOwner().GetLogin() + repoName := repo.GetName() + runs, _, err := client.Actions.ListWorkflowRunsByID( + ctx, + repoOwner, + repoName, + workflowID, + &github.ListWorkflowRunsOptions{ + Status: "completed", + HeadSHA: base.GetSHA(), + ExcludePullRequests: true, + }, + ) + if err != nil { + return errors.Wrap(err, "failed to list workflow runs by file name") + } + if len(runs.WorkflowRuns) == 0 { + logger.Debug(). + Str("commit_sha", base.GetSHA()). + Msg("No base workflow runs found") + return nil + } + + // Fetch report files for the PR base commit + baseRun := runs.WorkflowRuns[0] + baseFiles, err := objdiff.FetchReportFiles( + ctx, + logger, + client, + repoOwner, + repoName, + base.GetSHA(), + baseRun.GetID(), + ) + if err != nil { + return err + } + if len(baseFiles) == 0 { + logger.Info(). + Str("commit_sha", base.GetSHA()). + Int64("workflow_run_id", baseRun.GetID()). + Msg("No base report files found") + return nil + } + + // Generate changes for each report file + type versionChange struct { + Version string + Body string + } + versionChanges := make([]versionChange, 0) + for _, baseFile := range baseFiles { + for _, file := range files { + if baseFile.Version == file.Version { + logger := logger.With(). + Str("version", file.Version). + Str("from_sha", baseFile.Sha). + Str("to_sha", file.Sha). + Logger() + logger.Info().Msg("Generating changes") + changes, err := objdiff.GenerateChanges(config, logger, &baseFile, &file) + if err != nil { + logger.Error().Err(err).Msg("Failed to generate changes") + return err + } + body := createChanges(changes) + if body != "" { + versionChanges = append(versionChanges, versionChange{ + Version: file.Version, + Body: body, + }) + } + } + } + } + if len(versionChanges) == 0 { + logger.Info().Msg("No changes found") + return nil + } + + // Generate PR comment body + body := "## Changes\n\n" + for _, vc := range versionChanges { + body += fmt.Sprintf( + "
Version %s\n\n%s\n\n
\n\n", + vc.Version, + vc.Body, + ) + } + + // Update or create PR comment + err = upsertComment(ctx, client, repoOwner, repoName, prNum, body) + if err != nil { + return err + } + return nil +} + +func upsertComment( + ctx context.Context, + client *github.Client, + repoOwner, repoName string, + prNum int, + body string, +) error { + sort := "created" + direction := "asc" + existing, _, err := client.Issues.ListComments(ctx, repoOwner, repoName, prNum, &github.IssueListCommentsOptions{ + Sort: &sort, + Direction: &direction, + }) + if err != nil { + return errors.Wrap(err, "failed to list existing comments") + } + commentID := int64(0) + for _, comment := range existing { + // TODO: update go-github to expose performed_via_github_app + if comment.GetUser().GetLogin() == "decompal[bot]" { + commentID = comment.GetID() + if comment.GetBody() == body { + // No changes + return nil + } + break + } + } + if commentID != 0 { + _, _, err = client.Issues.EditComment(ctx, repoOwner, repoName, commentID, &github.IssueComment{ + Body: github.String(body), + }) + if err != nil { + return errors.Wrap(err, "failed to edit comment") + } + } else { + _, _, err = client.Issues.CreateComment(ctx, repoOwner, repoName, prNum, &github.IssueComment{ + Body: github.String(body), + }) + if err != nil { + return errors.Wrap(err, "failed to create comment") + } + } + return nil +} + +func createChanges(changes *objdiff.Changes) string { + out := "### Overall\n\n" + overallTable := changeInfoTable(changes.From, changes.To) + if overallTable == "" { + if len(changes.Units) == 0 { + return "" + } + out += "No changes\n\n" + } else { + out += overallTable + "\n\n" + } + for _, unit := range changes.Units { + out += fmt.Sprintf("---\n### `%s`\n\n", unit.Name) + unitTable := changeInfoTable(unit.From, unit.To) + if unitTable != "" { + out += unitTable + "\n\n" + } + functionsTable := changeItemTable("Functions", unit.Functions) + if functionsTable != "" { + out += functionsTable + "\n\n" + } + } + return out +} + +func changeItemTable(name string, items []*objdiff.ChangeItem) string { + header := fmt.Sprintf("|%s|Previous|Current|Change|\n|-|-|-|-|", name) + rows := make([]string, 0) + for _, item := range items { + row := changeItemInfoRow(item) + if row != "" { + rows = append(rows, row) + } + } + if len(rows) == 0 { + return "" + } + return header + "\n" + strings.Join(rows, "\n") +} + +const ( + incArrow = "${\\color{green}▲}$" + decArrow = "${\\color{red}▼}$" +) + +func floatArrow(diff float32) string { + if diff > 0 { + return " " + incArrow + } + if diff < 0 { + return " " + decArrow + } + return "" +} + +func intArrow(diff int64) string { + if diff > 0 { + return " " + incArrow + } + if diff < 0 { + return " " + decArrow + } + return "" +} + +func changeItemInfoRow(item *objdiff.ChangeItem) string { + var fromPercent, toPercent float32 + if item.From != nil { + fromPercent = item.From.FuzzyMatchPercent + } + if item.To != nil { + toPercent = item.To.FuzzyMatchPercent + } + if fromPercent == toPercent { + return "" + } + diff := toPercent - fromPercent + return fmt.Sprintf( + "|`%s`|%.2f%%|%.2f%%|%.2f%%%s|", + item.Name, + fromPercent, + toPercent, + diff, + floatArrow(diff), + ) +} + +func changeInfoTable(prev, curr *objdiff.ChangeInfo) string { + if prev == nil && curr == nil { + return "" + } else if prev == nil { + // TODO: added + prev = &objdiff.ChangeInfo{} + } else if curr == nil { + // TODO: removed + curr = &objdiff.ChangeInfo{} + } + header := "|Metric|Previous|Current|Change|\n|-|-|-|-|" + rows := make([]string, 0) + if prev.FuzzyMatchPercent != curr.FuzzyMatchPercent { + rows = append(rows, floatRow("Fuzzy match", prev.FuzzyMatchPercent, curr.FuzzyMatchPercent)) + } + if prev.TotalCode != curr.TotalCode { + rows = append(rows, sizeRow("Total code", prev.TotalCode, curr.TotalCode)) + } + if prev.MatchedCode != curr.MatchedCode || + prev.MatchedCodePercent != curr.MatchedCodePercent { + rows = append(rows, intPercentRow( + "Matched code", + prev.MatchedCode, + prev.MatchedCodePercent, + curr.MatchedCode, + curr.MatchedCodePercent, + )) + } + if prev.TotalData != curr.TotalData { + rows = append(rows, sizeRow("Total data", prev.TotalData, curr.TotalData)) + } + if prev.MatchedData != curr.MatchedData || + prev.MatchedDataPercent != curr.MatchedDataPercent { + rows = append(rows, intPercentRow( + "Matched data", + prev.MatchedData, + prev.MatchedDataPercent, + curr.MatchedData, + curr.MatchedDataPercent, + )) + } + if prev.TotalFunctions != curr.TotalFunctions { + rows = append(rows, intRow("Total functions", prev.TotalFunctions, curr.TotalFunctions)) + } + if prev.MatchedFunctions != curr.MatchedFunctions || + prev.MatchedFunctionsPercent != curr.MatchedFunctionsPercent { + rows = append(rows, intPercentRow( + "Matched functions", + uint64(prev.MatchedFunctions), + prev.MatchedFunctionsPercent, + uint64(curr.MatchedFunctions), + curr.MatchedFunctionsPercent, + )) + } + if len(rows) == 0 { + return "" + } + return header + "\n" + strings.Join(rows, "\n") +} + +func floatRow(name string, prev, curr float32) string { + diff := curr - prev + return fmt.Sprintf( + "|%s|%.2f%%|%.2f%%|%.2f%%%s|", + name, + prev, + curr, + diff, + floatArrow(diff), + ) +} + +func intRow(name string, prev, curr uint32) string { + diff := int64(curr) - int64(prev) + return fmt.Sprintf( + "|%s|%d|%d|%d%s|", + name, + prev, + curr, + diff, + intArrow(diff), + ) +} + +func sizeRow(name string, prev, curr uint64) string { + // TODO: format size + diff := int64(curr) - int64(prev) + return fmt.Sprintf( + "|%s|%d|%d|%d%s|", + name, + prev, + curr, + diff, + intArrow(diff), + ) +} + +func intPercentRow( + name string, + prevInt uint64, + prevPercent float32, + currInt uint64, + currPercent float32, +) string { + diff := int64(currInt) - int64(prevInt) + return fmt.Sprintf( + "|%s|%d (%.2f%%)|%d (%.2f%%)|%d (%.2f%%)%s|", + name, + prevInt, + prevPercent, + currInt, + currPercent, + diff, + currPercent-prevPercent, + intArrow(diff), + ) +} diff --git a/handlers/task.go b/handlers/task.go new file mode 100644 index 0000000..d8cdc2f --- /dev/null +++ b/handlers/task.go @@ -0,0 +1,22 @@ +package handlers + +import ( + "context" + "github.com/palantir/go-githubapp/githubapp" + "github.com/rs/zerolog" + "time" +) + +func backgroundTask(taskCtx context.Context, eventType, deliveryID string, run func(context.Context) error) { + logger := zerolog.Ctx(taskCtx).With(). + Str(githubapp.LogKeyDeliveryID, deliveryID). + Str(githubapp.LogKeyEventType, eventType). + Logger() + ctx, cancel := context.WithDeadline(taskCtx, time.Now().Add(time.Minute)) + defer cancel() + ctx = logger.WithContext(ctx) + err := run(ctx) + if err != nil { + logger.Error().Err(err).Msg("Background task failed") + } +} diff --git a/handlers/workflow_run.go b/handlers/workflow_run.go new file mode 100644 index 0000000..69b9f04 --- /dev/null +++ b/handlers/workflow_run.go @@ -0,0 +1,109 @@ +package handlers + +import ( + "context" + "encoding/json" + "github.com/encounter/decompal/config" + "github.com/encounter/decompal/objdiff" + "github.com/google/go-github/v63/github" + "github.com/palantir/go-githubapp/githubapp" + "github.com/pkg/errors" +) + +type workflowRunHandler struct { + githubapp.ClientCreator + config *config.AppConfig + taskCtx context.Context +} + +func NewWorkflowRunHandler( + cc githubapp.ClientCreator, + config *config.AppConfig, + taskCtx context.Context, +) githubapp.EventHandler { + return &workflowRunHandler{ + ClientCreator: cc, + config: config, + taskCtx: taskCtx, + } +} + +func (h *workflowRunHandler) Handles() []string { + return []string{"workflow_run"} +} + +func (h *workflowRunHandler) Handle(ctx context.Context, eventType, deliveryID string, payload []byte) error { + event := &github.WorkflowRunEvent{} + if err := json.Unmarshal(payload, event); err != nil { + return errors.Wrap(err, "failed to parse workflow run event payload") + } + + installationID := githubapp.GetInstallationIDFromEvent(event) + ctx, logger := githubapp.PrepareRepoContext(ctx, installationID, event.GetRepo()) + status := event.GetWorkflowRun().GetStatus() + if status != "completed" { + logger.Debug(). + Str("status", status). + Msg("Workflow run event is not completed") + return nil + } + + client, err := h.NewInstallationClient(installationID) + if err != nil { + return err + } + + go backgroundTask(h.taskCtx, eventType, deliveryID, func(ctx context.Context) error { + repo := event.GetRepo() + // Re-prepare logger instead of using the one from the request context + ctx, logger := githubapp.PrepareRepoContext(ctx, installationID, repo) + + // Fetch report files for the current workflow run + repoOwner := repo.GetOwner().GetLogin() + repoName := repo.GetName() + run := event.GetWorkflowRun() + runId := run.GetID() + sha := run.GetHeadSHA() + files, err := objdiff.FetchReportFiles( + ctx, + logger, + client, + repoOwner, + repoName, + sha, + runId, + ) + if err != nil { + return err + } + if len(files) == 0 { + logger.Info().Msg("No report files found") + return nil + } + + // Process all pull requests associated with the workflow run + prs := event.GetWorkflowRun().PullRequests + if prs != nil { + for _, pr := range prs { + err = processPR( + ctx, + h.config, + installationID, + pr, + sha, + client, + repo, + run.GetWorkflowID(), + files, + ) + if err != nil { + return err + } + } + } + + return nil + }) + + return nil +} diff --git a/objdiff/changes.go b/objdiff/changes.go new file mode 100644 index 0000000..4fa662e --- /dev/null +++ b/objdiff/changes.go @@ -0,0 +1,65 @@ +package objdiff + +import ( + "github.com/encounter/decompal/config" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "google.golang.org/protobuf/proto" + "os/exec" +) + +func GenerateChanges( + config *config.AppConfig, + logger zerolog.Logger, + prev *ReportFile, + curr *ReportFile, +) (*Changes, error) { + if config.ObjdiffPath == "" { + return nil, errors.New("objdiff_path not set") + } + + data, err := proto.Marshal(&ChangesInput{ + From: prev.Report, + To: curr.Report, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to encode changes input") + } + + // Run objdiff with proto input and output + // The `--` is to delimit the end of flags and the start of positional arguments + // Otherwise the argument parser gets confused + cmd := exec.Command( + config.ObjdiffPath, + "report", + "changes", + "-f", + "proto", + "--", + "-", + "-", + ) + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, errors.Wrap(err, "failed to open stdin pipe") + } + go func() { + defer stdin.Close() + if _, err := stdin.Write(data); err != nil { + logger.Err(err).Msg("failed to write data to objdiff") + } + }() + // It shouldn't have any stderr output when successful, but we want to capture errors + // May need to change this if objdiff changes + output, err := cmd.CombinedOutput() + if err != nil { + return nil, errors.Wrapf(err, "failed to generate changes: %s", string(output)) + } + + changes := &Changes{} + err = proto.Unmarshal(output, changes) + if err != nil { + return nil, errors.Wrap(err, "failed to decode changes file") + } + return changes, nil +} diff --git a/objdiff/legacy.go b/objdiff/legacy.go new file mode 100644 index 0000000..b95a994 --- /dev/null +++ b/objdiff/legacy.go @@ -0,0 +1,109 @@ +package objdiff + +import ( + "strconv" + "strings" +) + +// Older JSON report types +type legacyReport struct { + FuzzyMatchPercent float32 `json:"fuzzy_match_percent"` + TotalCode uint64 `json:"total_code"` + MatchedCode uint64 `json:"matched_code"` + MatchedCodePercent float32 `json:"matched_code_percent"` + TotalData uint64 `json:"total_data"` + MatchedData uint64 `json:"matched_data"` + MatchedDataPercent float32 `json:"matched_data_percent"` + TotalFunctions uint32 `json:"total_functions"` + MatchedFunctions uint32 `json:"matched_functions"` + MatchedFunctionsPercent float32 `json:"matched_functions_percent"` + Units []legacyReportUnit `json:"units"` +} + +type legacyReportUnit struct { + Name string `json:"name"` + FuzzyMatchPercent float32 `json:"fuzzy_match_percent"` + TotalCode uint64 `json:"total_code"` + MatchedCode uint64 `json:"matched_code"` + TotalData uint64 `json:"total_data"` + MatchedData uint64 `json:"matched_data"` + TotalFunctions uint32 `json:"total_functions"` + MatchedFunctions uint32 `json:"matched_functions"` + Complete *bool `json:"complete,omitempty"` + ModuleName *string `json:"module_name,omitempty"` + ModuleID *uint32 `json:"module_id,omitempty"` + Sections []legacyReportItem `json:"sections"` + Functions []legacyReportItem `json:"functions"` +} + +type legacyReportItem struct { + Name string `json:"name"` + DemangledName *string `json:"demangled_name,omitempty"` + Address *string `json:"address,omitempty"` // hex string + Size uint64 `json:"size"` + FuzzyMatchPercent float32 `json:"fuzzy_match_percent"` +} + +func (r *legacyReport) convert() *Report { + report := &Report{ + FuzzyMatchPercent: r.FuzzyMatchPercent, + TotalCode: r.TotalCode, + MatchedCode: r.MatchedCode, + MatchedCodePercent: r.MatchedCodePercent, + TotalData: r.TotalData, + MatchedData: r.MatchedData, + MatchedDataPercent: r.MatchedDataPercent, + TotalFunctions: r.TotalFunctions, + MatchedFunctions: r.MatchedFunctions, + MatchedFunctionsPercent: r.MatchedFunctionsPercent, + Units: make([]*ReportUnit, 0, len(r.Units)), + } + for _, unit := range r.Units { + report.Units = append(report.Units, unit.convert()) + } + return report +} + +func (u *legacyReportUnit) convert() *ReportUnit { + unit := &ReportUnit{ + Name: u.Name, + FuzzyMatchPercent: u.FuzzyMatchPercent, + TotalCode: u.TotalCode, + MatchedCode: u.MatchedCode, + TotalData: u.TotalData, + MatchedData: u.MatchedData, + TotalFunctions: u.TotalFunctions, + MatchedFunctions: u.MatchedFunctions, + Complete: u.Complete, + ModuleName: u.ModuleName, + ModuleId: u.ModuleID, + Sections: make([]*ReportItem, 0, len(u.Sections)), + Functions: make([]*ReportItem, 0, len(u.Functions)), + } + for _, section := range u.Sections { + unit.Sections = append(unit.Sections, section.convert()) + } + for _, function := range u.Functions { + unit.Functions = append(unit.Functions, function.convert()) + } + return unit +} + +func (i *legacyReportItem) convert() *ReportItem { + var address uint64 + if i.Address != nil { + addressStr := *i.Address + if strings.HasPrefix(addressStr, "0x") { + address, _ = strconv.ParseUint(addressStr[2:], 16, 64) + } else { + address, _ = strconv.ParseUint(addressStr, 10, 64) + } + } + return &ReportItem{ + Name: i.Name, + DemangledName: i.DemangledName, + Address: &address, + Size: i.Size, + FuzzyMatchPercent: i.FuzzyMatchPercent, + } +} diff --git a/objdiff/report.go b/objdiff/report.go new file mode 100644 index 0000000..41c3c43 --- /dev/null +++ b/objdiff/report.go @@ -0,0 +1,153 @@ +package objdiff + +import ( + "context" + "encoding/json" + "github.com/encounter/decompal/zipstream" + "github.com/google/go-github/v63/github" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "io" + "net/http" + "regexp" + "sort" + "strings" +) + +type ReportFile struct { + Version string + Sha string + Report *Report +} + +var artifactNameRegex = regexp.MustCompile(`^(?P[A-z0-9_\-]+)_report$`) + +func FetchReportFiles( + ctx context.Context, + logger zerolog.Logger, + client *github.Client, + repoOwner, repoName, sha string, + runId int64, +) ([]ReportFile, error) { + logger = logger.With(). + Str("commit_sha", sha). + Int64("workflow_run_id", runId). + Logger() + + artifacts, _, err := client.Actions.ListWorkflowRunArtifacts(ctx, repoOwner, repoName, runId, nil) + if err != nil { + logger.Error(). + Err(err). + Msg("Failed to list workflow run artifacts") + return nil, errors.Wrap(err, "failed to list workflow run artifacts") + } + + files := make([]ReportFile, 0) + for _, artifact := range artifacts.Artifacts { + logger := logger.With(). + Str("artifact_name", artifact.GetName()). + Int64("artifact_id", artifact.GetID()). + Logger() + + matches := artifactNameRegex.FindStringSubmatch(artifact.GetName()) + if matches == nil { + //logger.Debug().Msg("Skipping artifact") + continue + } + version := matches[artifactNameRegex.SubexpIndex("version")] + + artifactUrl, _, err := client.Actions.DownloadArtifact(ctx, repoOwner, repoName, artifact.GetID(), 3) + if err != nil { + return nil, errors.Wrap(err, "failed to get artifact download url") + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, artifactUrl.String(), nil) + if err != nil { + return nil, errors.Wrap(err, "failed to create download request") + } + + req.Header.Set("User-Agent", client.UserAgent) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, errors.Wrap(err, "failed to download artifact") + } + + report, err := findReportFile(logger, resp.Body) + _ = resp.Body.Close() + if err != nil { + return nil, err + } + if report != nil { + files = append(files, ReportFile{ + Version: version, + Sha: sha, + Report: report, + }) + } + } + + // Sort files by version + sort.Slice(files, func(i, j int) bool { + return files[i].Version < files[j].Version + }) + return files, nil +} + +// findReportFile reads the zip stream and writes the report file to the output path +// Returns true if the report file was found and written +func findReportFile(logger zerolog.Logger, r io.Reader) (*Report, error) { + zr := zipstream.NewReader(r) + for { + entry, err := zr.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, errors.Wrap(err, "failed to get next entry") + } + + data, err := io.ReadAll(entry) + if err != nil { + return nil, errors.Wrap(err, "failed to read report file") + } + if strings.HasSuffix(entry.Name, "report.json") { + report := &Report{} + err := parseJson(data, report) + if err != nil { + return nil, errors.Wrap(err, "failed to read report file") + } + logger.Info(). + Str("filename", entry.Name). + Msg("Read report file") + return report, nil + } else if strings.HasSuffix(entry.Name, "report.binpb") || + strings.HasSuffix(entry.Name, "report.pb") { + report := &Report{} + err = proto.Unmarshal(data, report) + if err != nil { + return nil, err + } + logger.Info(). + Str("filename", entry.Name). + Msg("Read report file") + return report, nil + } + } + return nil, nil +} + +func parseJson(data []byte, v *Report) error { + err := protojson.Unmarshal(data, v) + if err != nil { + // Try to parse as legacy report + legacy := &legacyReport{} + if other := json.Unmarshal(data, legacy); other != nil { + // Return the original error + return err + } + *v = *legacy.convert() + } + return nil +} diff --git a/objdiff/report.pb.go b/objdiff/report.pb.go new file mode 100644 index 0000000..a51f5cb --- /dev/null +++ b/objdiff/report.pb.go @@ -0,0 +1,1152 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v5.27.3 +// source: report.proto + +package objdiff + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Report struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FuzzyMatchPercent float32 `protobuf:"fixed32,1,opt,name=fuzzy_match_percent,json=fuzzyMatchPercent,proto3" json:"fuzzy_match_percent,omitempty"` + TotalCode uint64 `protobuf:"varint,2,opt,name=total_code,json=totalCode,proto3" json:"total_code,omitempty"` + MatchedCode uint64 `protobuf:"varint,3,opt,name=matched_code,json=matchedCode,proto3" json:"matched_code,omitempty"` + MatchedCodePercent float32 `protobuf:"fixed32,4,opt,name=matched_code_percent,json=matchedCodePercent,proto3" json:"matched_code_percent,omitempty"` + TotalData uint64 `protobuf:"varint,5,opt,name=total_data,json=totalData,proto3" json:"total_data,omitempty"` + MatchedData uint64 `protobuf:"varint,6,opt,name=matched_data,json=matchedData,proto3" json:"matched_data,omitempty"` + MatchedDataPercent float32 `protobuf:"fixed32,7,opt,name=matched_data_percent,json=matchedDataPercent,proto3" json:"matched_data_percent,omitempty"` + TotalFunctions uint32 `protobuf:"varint,8,opt,name=total_functions,json=totalFunctions,proto3" json:"total_functions,omitempty"` + MatchedFunctions uint32 `protobuf:"varint,9,opt,name=matched_functions,json=matchedFunctions,proto3" json:"matched_functions,omitempty"` + MatchedFunctionsPercent float32 `protobuf:"fixed32,10,opt,name=matched_functions_percent,json=matchedFunctionsPercent,proto3" json:"matched_functions_percent,omitempty"` + Units []*ReportUnit `protobuf:"bytes,11,rep,name=units,proto3" json:"units,omitempty"` +} + +func (x *Report) Reset() { + *x = Report{} + if protoimpl.UnsafeEnabled { + mi := &file_report_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Report) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Report) ProtoMessage() {} + +func (x *Report) ProtoReflect() protoreflect.Message { + mi := &file_report_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Report.ProtoReflect.Descriptor instead. +func (*Report) Descriptor() ([]byte, []int) { + return file_report_proto_rawDescGZIP(), []int{0} +} + +func (x *Report) GetFuzzyMatchPercent() float32 { + if x != nil { + return x.FuzzyMatchPercent + } + return 0 +} + +func (x *Report) GetTotalCode() uint64 { + if x != nil { + return x.TotalCode + } + return 0 +} + +func (x *Report) GetMatchedCode() uint64 { + if x != nil { + return x.MatchedCode + } + return 0 +} + +func (x *Report) GetMatchedCodePercent() float32 { + if x != nil { + return x.MatchedCodePercent + } + return 0 +} + +func (x *Report) GetTotalData() uint64 { + if x != nil { + return x.TotalData + } + return 0 +} + +func (x *Report) GetMatchedData() uint64 { + if x != nil { + return x.MatchedData + } + return 0 +} + +func (x *Report) GetMatchedDataPercent() float32 { + if x != nil { + return x.MatchedDataPercent + } + return 0 +} + +func (x *Report) GetTotalFunctions() uint32 { + if x != nil { + return x.TotalFunctions + } + return 0 +} + +func (x *Report) GetMatchedFunctions() uint32 { + if x != nil { + return x.MatchedFunctions + } + return 0 +} + +func (x *Report) GetMatchedFunctionsPercent() float32 { + if x != nil { + return x.MatchedFunctionsPercent + } + return 0 +} + +func (x *Report) GetUnits() []*ReportUnit { + if x != nil { + return x.Units + } + return nil +} + +type ReportUnit struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + FuzzyMatchPercent float32 `protobuf:"fixed32,2,opt,name=fuzzy_match_percent,json=fuzzyMatchPercent,proto3" json:"fuzzy_match_percent,omitempty"` + TotalCode uint64 `protobuf:"varint,3,opt,name=total_code,json=totalCode,proto3" json:"total_code,omitempty"` + MatchedCode uint64 `protobuf:"varint,4,opt,name=matched_code,json=matchedCode,proto3" json:"matched_code,omitempty"` + TotalData uint64 `protobuf:"varint,5,opt,name=total_data,json=totalData,proto3" json:"total_data,omitempty"` + MatchedData uint64 `protobuf:"varint,6,opt,name=matched_data,json=matchedData,proto3" json:"matched_data,omitempty"` + TotalFunctions uint32 `protobuf:"varint,7,opt,name=total_functions,json=totalFunctions,proto3" json:"total_functions,omitempty"` + MatchedFunctions uint32 `protobuf:"varint,8,opt,name=matched_functions,json=matchedFunctions,proto3" json:"matched_functions,omitempty"` + Complete *bool `protobuf:"varint,9,opt,name=complete,proto3,oneof" json:"complete,omitempty"` + ModuleName *string `protobuf:"bytes,10,opt,name=module_name,json=moduleName,proto3,oneof" json:"module_name,omitempty"` + ModuleId *uint32 `protobuf:"varint,11,opt,name=module_id,json=moduleId,proto3,oneof" json:"module_id,omitempty"` + Sections []*ReportItem `protobuf:"bytes,12,rep,name=sections,proto3" json:"sections,omitempty"` + Functions []*ReportItem `protobuf:"bytes,13,rep,name=functions,proto3" json:"functions,omitempty"` +} + +func (x *ReportUnit) Reset() { + *x = ReportUnit{} + if protoimpl.UnsafeEnabled { + mi := &file_report_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReportUnit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportUnit) ProtoMessage() {} + +func (x *ReportUnit) ProtoReflect() protoreflect.Message { + mi := &file_report_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReportUnit.ProtoReflect.Descriptor instead. +func (*ReportUnit) Descriptor() ([]byte, []int) { + return file_report_proto_rawDescGZIP(), []int{1} +} + +func (x *ReportUnit) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ReportUnit) GetFuzzyMatchPercent() float32 { + if x != nil { + return x.FuzzyMatchPercent + } + return 0 +} + +func (x *ReportUnit) GetTotalCode() uint64 { + if x != nil { + return x.TotalCode + } + return 0 +} + +func (x *ReportUnit) GetMatchedCode() uint64 { + if x != nil { + return x.MatchedCode + } + return 0 +} + +func (x *ReportUnit) GetTotalData() uint64 { + if x != nil { + return x.TotalData + } + return 0 +} + +func (x *ReportUnit) GetMatchedData() uint64 { + if x != nil { + return x.MatchedData + } + return 0 +} + +func (x *ReportUnit) GetTotalFunctions() uint32 { + if x != nil { + return x.TotalFunctions + } + return 0 +} + +func (x *ReportUnit) GetMatchedFunctions() uint32 { + if x != nil { + return x.MatchedFunctions + } + return 0 +} + +func (x *ReportUnit) GetComplete() bool { + if x != nil && x.Complete != nil { + return *x.Complete + } + return false +} + +func (x *ReportUnit) GetModuleName() string { + if x != nil && x.ModuleName != nil { + return *x.ModuleName + } + return "" +} + +func (x *ReportUnit) GetModuleId() uint32 { + if x != nil && x.ModuleId != nil { + return *x.ModuleId + } + return 0 +} + +func (x *ReportUnit) GetSections() []*ReportItem { + if x != nil { + return x.Sections + } + return nil +} + +func (x *ReportUnit) GetFunctions() []*ReportItem { + if x != nil { + return x.Functions + } + return nil +} + +type ReportItem struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Size uint64 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` + FuzzyMatchPercent float32 `protobuf:"fixed32,3,opt,name=fuzzy_match_percent,json=fuzzyMatchPercent,proto3" json:"fuzzy_match_percent,omitempty"` + DemangledName *string `protobuf:"bytes,4,opt,name=demangled_name,json=demangledName,proto3,oneof" json:"demangled_name,omitempty"` + Address *uint64 `protobuf:"varint,5,opt,name=address,proto3,oneof" json:"address,omitempty"` +} + +func (x *ReportItem) Reset() { + *x = ReportItem{} + if protoimpl.UnsafeEnabled { + mi := &file_report_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReportItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportItem) ProtoMessage() {} + +func (x *ReportItem) ProtoReflect() protoreflect.Message { + mi := &file_report_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReportItem.ProtoReflect.Descriptor instead. +func (*ReportItem) Descriptor() ([]byte, []int) { + return file_report_proto_rawDescGZIP(), []int{2} +} + +func (x *ReportItem) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ReportItem) GetSize() uint64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *ReportItem) GetFuzzyMatchPercent() float32 { + if x != nil { + return x.FuzzyMatchPercent + } + return 0 +} + +func (x *ReportItem) GetDemangledName() string { + if x != nil && x.DemangledName != nil { + return *x.DemangledName + } + return "" +} + +func (x *ReportItem) GetAddress() uint64 { + if x != nil && x.Address != nil { + return *x.Address + } + return 0 +} + +// Used as stdin for the changes command +type ChangesInput struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + From *Report `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` + To *Report `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"` +} + +func (x *ChangesInput) Reset() { + *x = ChangesInput{} + if protoimpl.UnsafeEnabled { + mi := &file_report_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChangesInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangesInput) ProtoMessage() {} + +func (x *ChangesInput) ProtoReflect() protoreflect.Message { + mi := &file_report_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangesInput.ProtoReflect.Descriptor instead. +func (*ChangesInput) Descriptor() ([]byte, []int) { + return file_report_proto_rawDescGZIP(), []int{3} +} + +func (x *ChangesInput) GetFrom() *Report { + if x != nil { + return x.From + } + return nil +} + +func (x *ChangesInput) GetTo() *Report { + if x != nil { + return x.To + } + return nil +} + +type Changes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + From *ChangeInfo `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` + To *ChangeInfo `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"` + Units []*ChangeUnit `protobuf:"bytes,3,rep,name=units,proto3" json:"units,omitempty"` +} + +func (x *Changes) Reset() { + *x = Changes{} + if protoimpl.UnsafeEnabled { + mi := &file_report_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Changes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Changes) ProtoMessage() {} + +func (x *Changes) ProtoReflect() protoreflect.Message { + mi := &file_report_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Changes.ProtoReflect.Descriptor instead. +func (*Changes) Descriptor() ([]byte, []int) { + return file_report_proto_rawDescGZIP(), []int{4} +} + +func (x *Changes) GetFrom() *ChangeInfo { + if x != nil { + return x.From + } + return nil +} + +func (x *Changes) GetTo() *ChangeInfo { + if x != nil { + return x.To + } + return nil +} + +func (x *Changes) GetUnits() []*ChangeUnit { + if x != nil { + return x.Units + } + return nil +} + +type ChangeInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FuzzyMatchPercent float32 `protobuf:"fixed32,1,opt,name=fuzzy_match_percent,json=fuzzyMatchPercent,proto3" json:"fuzzy_match_percent,omitempty"` + TotalCode uint64 `protobuf:"varint,2,opt,name=total_code,json=totalCode,proto3" json:"total_code,omitempty"` + MatchedCode uint64 `protobuf:"varint,3,opt,name=matched_code,json=matchedCode,proto3" json:"matched_code,omitempty"` + MatchedCodePercent float32 `protobuf:"fixed32,4,opt,name=matched_code_percent,json=matchedCodePercent,proto3" json:"matched_code_percent,omitempty"` + TotalData uint64 `protobuf:"varint,5,opt,name=total_data,json=totalData,proto3" json:"total_data,omitempty"` + MatchedData uint64 `protobuf:"varint,6,opt,name=matched_data,json=matchedData,proto3" json:"matched_data,omitempty"` + MatchedDataPercent float32 `protobuf:"fixed32,7,opt,name=matched_data_percent,json=matchedDataPercent,proto3" json:"matched_data_percent,omitempty"` + TotalFunctions uint32 `protobuf:"varint,8,opt,name=total_functions,json=totalFunctions,proto3" json:"total_functions,omitempty"` + MatchedFunctions uint32 `protobuf:"varint,9,opt,name=matched_functions,json=matchedFunctions,proto3" json:"matched_functions,omitempty"` + MatchedFunctionsPercent float32 `protobuf:"fixed32,10,opt,name=matched_functions_percent,json=matchedFunctionsPercent,proto3" json:"matched_functions_percent,omitempty"` +} + +func (x *ChangeInfo) Reset() { + *x = ChangeInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_report_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChangeInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeInfo) ProtoMessage() {} + +func (x *ChangeInfo) ProtoReflect() protoreflect.Message { + mi := &file_report_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangeInfo.ProtoReflect.Descriptor instead. +func (*ChangeInfo) Descriptor() ([]byte, []int) { + return file_report_proto_rawDescGZIP(), []int{5} +} + +func (x *ChangeInfo) GetFuzzyMatchPercent() float32 { + if x != nil { + return x.FuzzyMatchPercent + } + return 0 +} + +func (x *ChangeInfo) GetTotalCode() uint64 { + if x != nil { + return x.TotalCode + } + return 0 +} + +func (x *ChangeInfo) GetMatchedCode() uint64 { + if x != nil { + return x.MatchedCode + } + return 0 +} + +func (x *ChangeInfo) GetMatchedCodePercent() float32 { + if x != nil { + return x.MatchedCodePercent + } + return 0 +} + +func (x *ChangeInfo) GetTotalData() uint64 { + if x != nil { + return x.TotalData + } + return 0 +} + +func (x *ChangeInfo) GetMatchedData() uint64 { + if x != nil { + return x.MatchedData + } + return 0 +} + +func (x *ChangeInfo) GetMatchedDataPercent() float32 { + if x != nil { + return x.MatchedDataPercent + } + return 0 +} + +func (x *ChangeInfo) GetTotalFunctions() uint32 { + if x != nil { + return x.TotalFunctions + } + return 0 +} + +func (x *ChangeInfo) GetMatchedFunctions() uint32 { + if x != nil { + return x.MatchedFunctions + } + return 0 +} + +func (x *ChangeInfo) GetMatchedFunctionsPercent() float32 { + if x != nil { + return x.MatchedFunctionsPercent + } + return 0 +} + +type ChangeUnit struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + From *ChangeInfo `protobuf:"bytes,2,opt,name=from,proto3,oneof" json:"from,omitempty"` + To *ChangeInfo `protobuf:"bytes,3,opt,name=to,proto3,oneof" json:"to,omitempty"` + Sections []*ChangeItem `protobuf:"bytes,4,rep,name=sections,proto3" json:"sections,omitempty"` + Functions []*ChangeItem `protobuf:"bytes,5,rep,name=functions,proto3" json:"functions,omitempty"` +} + +func (x *ChangeUnit) Reset() { + *x = ChangeUnit{} + if protoimpl.UnsafeEnabled { + mi := &file_report_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChangeUnit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeUnit) ProtoMessage() {} + +func (x *ChangeUnit) ProtoReflect() protoreflect.Message { + mi := &file_report_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangeUnit.ProtoReflect.Descriptor instead. +func (*ChangeUnit) Descriptor() ([]byte, []int) { + return file_report_proto_rawDescGZIP(), []int{6} +} + +func (x *ChangeUnit) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ChangeUnit) GetFrom() *ChangeInfo { + if x != nil { + return x.From + } + return nil +} + +func (x *ChangeUnit) GetTo() *ChangeInfo { + if x != nil { + return x.To + } + return nil +} + +func (x *ChangeUnit) GetSections() []*ChangeItem { + if x != nil { + return x.Sections + } + return nil +} + +func (x *ChangeUnit) GetFunctions() []*ChangeItem { + if x != nil { + return x.Functions + } + return nil +} + +type ChangeItem struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + From *ChangeItemInfo `protobuf:"bytes,2,opt,name=from,proto3,oneof" json:"from,omitempty"` + To *ChangeItemInfo `protobuf:"bytes,3,opt,name=to,proto3,oneof" json:"to,omitempty"` +} + +func (x *ChangeItem) Reset() { + *x = ChangeItem{} + if protoimpl.UnsafeEnabled { + mi := &file_report_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChangeItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeItem) ProtoMessage() {} + +func (x *ChangeItem) ProtoReflect() protoreflect.Message { + mi := &file_report_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangeItem.ProtoReflect.Descriptor instead. +func (*ChangeItem) Descriptor() ([]byte, []int) { + return file_report_proto_rawDescGZIP(), []int{7} +} + +func (x *ChangeItem) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ChangeItem) GetFrom() *ChangeItemInfo { + if x != nil { + return x.From + } + return nil +} + +func (x *ChangeItem) GetTo() *ChangeItemInfo { + if x != nil { + return x.To + } + return nil +} + +type ChangeItemInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FuzzyMatchPercent float32 `protobuf:"fixed32,1,opt,name=fuzzy_match_percent,json=fuzzyMatchPercent,proto3" json:"fuzzy_match_percent,omitempty"` + Size uint64 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` +} + +func (x *ChangeItemInfo) Reset() { + *x = ChangeItemInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_report_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChangeItemInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeItemInfo) ProtoMessage() {} + +func (x *ChangeItemInfo) ProtoReflect() protoreflect.Message { + mi := &file_report_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangeItemInfo.ProtoReflect.Descriptor instead. +func (*ChangeItemInfo) Descriptor() ([]byte, []int) { + return file_report_proto_rawDescGZIP(), []int{8} +} + +func (x *ChangeItemInfo) GetFuzzyMatchPercent() float32 { + if x != nil { + return x.FuzzyMatchPercent + } + return 0 +} + +func (x *ChangeItemInfo) GetSize() uint64 { + if x != nil { + return x.Size + } + return 0 +} + +var File_report_proto protoreflect.FileDescriptor + +var file_report_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, + 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x22, 0xe4, + 0x03, 0x0a, 0x06, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x75, 0x7a, + 0x7a, 0x79, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x02, 0x52, 0x11, 0x66, 0x75, 0x7a, 0x7a, 0x79, 0x4d, 0x61, 0x74, + 0x63, 0x68, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x74, 0x63, + 0x68, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, + 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x30, 0x0a, 0x14, 0x6d, + 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x63, + 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x02, 0x52, 0x12, 0x6d, 0x61, 0x74, 0x63, 0x68, + 0x65, 0x64, 0x43, 0x6f, 0x64, 0x65, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x0a, + 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, + 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0b, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, + 0x30, 0x0a, 0x14, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, + 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x02, 0x52, 0x12, 0x6d, + 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, + 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x6d, 0x61, + 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x46, 0x75, + 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3a, 0x0a, 0x19, 0x6d, 0x61, 0x74, 0x63, 0x68, + 0x65, 0x64, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x70, 0x65, 0x72, + 0x63, 0x65, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x02, 0x52, 0x17, 0x6d, 0x61, 0x74, 0x63, + 0x68, 0x65, 0x64, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x50, 0x65, 0x72, 0x63, + 0x65, 0x6e, 0x74, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x18, 0x0b, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x55, 0x6e, 0x69, 0x74, 0x52, 0x05, + 0x75, 0x6e, 0x69, 0x74, 0x73, 0x22, 0xb0, 0x04, 0x0a, 0x0a, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x55, 0x6e, 0x69, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x75, 0x7a, 0x7a, + 0x79, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x11, 0x66, 0x75, 0x7a, 0x7a, 0x79, 0x4d, 0x61, 0x74, 0x63, + 0x68, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x74, 0x63, 0x68, + 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6d, + 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x74, + 0x63, 0x68, 0x65, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x0b, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, 0x27, 0x0a, 0x0f, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x75, 0x6e, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, + 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x10, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x1f, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, + 0x88, 0x01, 0x01, 0x12, 0x24, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x75, + 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x6d, 0x6f, 0x64, + 0x75, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, + 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x36, 0x0a, 0x08, 0x73, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x08, 0x73, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x38, 0x0a, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, 0x66, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x74, + 0x65, 0x6d, 0x52, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x0b, 0x0a, + 0x09, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x6d, + 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x6d, + 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x22, 0xce, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, + 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, + 0x2e, 0x0a, 0x13, 0x66, 0x75, 0x7a, 0x7a, 0x79, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x70, + 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x11, 0x66, 0x75, + 0x7a, 0x7a, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, + 0x2a, 0x0a, 0x0e, 0x64, 0x65, 0x6d, 0x61, 0x6e, 0x67, 0x6c, 0x65, 0x64, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x64, 0x65, 0x6d, 0x61, 0x6e, + 0x67, 0x6c, 0x65, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x61, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x48, 0x01, 0x52, 0x07, + 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x88, 0x01, 0x01, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x64, + 0x65, 0x6d, 0x61, 0x6e, 0x67, 0x6c, 0x65, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0a, 0x0a, + 0x08, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x62, 0x0a, 0x0c, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x73, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x2a, 0x0a, 0x04, 0x66, 0x72, 0x6f, + 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, + 0x66, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, + 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x26, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x02, 0x74, 0x6f, 0x22, 0x97, 0x01, + 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x04, 0x66, 0x72, 0x6f, + 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, + 0x66, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x49, + 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x2a, 0x0a, 0x02, 0x74, 0x6f, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, 0x66, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x49, 0x6e, 0x66, + 0x6f, 0x52, 0x02, 0x74, 0x6f, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x55, 0x6e, 0x69, 0x74, + 0x52, 0x05, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x22, 0xb6, 0x03, 0x0a, 0x0a, 0x43, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x75, 0x7a, 0x7a, 0x79, 0x5f, + 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x02, 0x52, 0x11, 0x66, 0x75, 0x7a, 0x7a, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x50, + 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, + 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, + 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6d, 0x61, 0x74, + 0x63, 0x68, 0x65, 0x64, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x30, 0x0a, 0x14, 0x6d, 0x61, 0x74, 0x63, + 0x68, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x02, 0x52, 0x12, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x43, + 0x6f, 0x64, 0x65, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x74, + 0x63, 0x68, 0x65, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x0b, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, 0x30, 0x0a, 0x14, + 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x70, 0x65, 0x72, + 0x63, 0x65, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x02, 0x52, 0x12, 0x6d, 0x61, 0x74, 0x63, + 0x68, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x27, + 0x0a, 0x0f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x75, + 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x6d, 0x61, 0x74, 0x63, 0x68, + 0x65, 0x64, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x10, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x46, 0x75, 0x6e, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3a, 0x0a, 0x19, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, + 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, + 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x02, 0x52, 0x17, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, + 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, + 0x22, 0x88, 0x02, 0x0a, 0x0a, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x55, 0x6e, 0x69, 0x74, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x00, 0x52, + 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x49, 0x6e, 0x66, 0x6f, + 0x48, 0x01, 0x52, 0x02, 0x74, 0x6f, 0x88, 0x01, 0x01, 0x12, 0x36, 0x0a, 0x08, 0x73, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6f, 0x62, + 0x6a, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x08, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x38, 0x0a, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x49, 0x74, 0x65, 0x6d, + 0x52, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x5f, + 0x66, 0x72, 0x6f, 0x6d, 0x42, 0x05, 0x0a, 0x03, 0x5f, 0x74, 0x6f, 0x22, 0x9e, 0x01, 0x0a, 0x0a, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x37, + 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6f, + 0x62, 0x6a, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x00, 0x52, 0x04, + 0x66, 0x72, 0x6f, 0x6d, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6f, 0x62, 0x6a, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x49, + 0x6e, 0x66, 0x6f, 0x48, 0x01, 0x52, 0x02, 0x74, 0x6f, 0x88, 0x01, 0x01, 0x42, 0x07, 0x0a, 0x05, + 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x42, 0x05, 0x0a, 0x03, 0x5f, 0x74, 0x6f, 0x22, 0x54, 0x0a, 0x0e, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2e, + 0x0a, 0x13, 0x66, 0x75, 0x7a, 0x7a, 0x79, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x70, 0x65, + 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x02, 0x52, 0x11, 0x66, 0x75, 0x7a, + 0x7a, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x12, + 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x69, + 0x7a, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_report_proto_rawDescOnce sync.Once + file_report_proto_rawDescData = file_report_proto_rawDesc +) + +func file_report_proto_rawDescGZIP() []byte { + file_report_proto_rawDescOnce.Do(func() { + file_report_proto_rawDescData = protoimpl.X.CompressGZIP(file_report_proto_rawDescData) + }) + return file_report_proto_rawDescData +} + +var file_report_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_report_proto_goTypes = []any{ + (*Report)(nil), // 0: objdiff.report.Report + (*ReportUnit)(nil), // 1: objdiff.report.ReportUnit + (*ReportItem)(nil), // 2: objdiff.report.ReportItem + (*ChangesInput)(nil), // 3: objdiff.report.ChangesInput + (*Changes)(nil), // 4: objdiff.report.Changes + (*ChangeInfo)(nil), // 5: objdiff.report.ChangeInfo + (*ChangeUnit)(nil), // 6: objdiff.report.ChangeUnit + (*ChangeItem)(nil), // 7: objdiff.report.ChangeItem + (*ChangeItemInfo)(nil), // 8: objdiff.report.ChangeItemInfo +} +var file_report_proto_depIdxs = []int32{ + 1, // 0: objdiff.report.Report.units:type_name -> objdiff.report.ReportUnit + 2, // 1: objdiff.report.ReportUnit.sections:type_name -> objdiff.report.ReportItem + 2, // 2: objdiff.report.ReportUnit.functions:type_name -> objdiff.report.ReportItem + 0, // 3: objdiff.report.ChangesInput.from:type_name -> objdiff.report.Report + 0, // 4: objdiff.report.ChangesInput.to:type_name -> objdiff.report.Report + 5, // 5: objdiff.report.Changes.from:type_name -> objdiff.report.ChangeInfo + 5, // 6: objdiff.report.Changes.to:type_name -> objdiff.report.ChangeInfo + 6, // 7: objdiff.report.Changes.units:type_name -> objdiff.report.ChangeUnit + 5, // 8: objdiff.report.ChangeUnit.from:type_name -> objdiff.report.ChangeInfo + 5, // 9: objdiff.report.ChangeUnit.to:type_name -> objdiff.report.ChangeInfo + 7, // 10: objdiff.report.ChangeUnit.sections:type_name -> objdiff.report.ChangeItem + 7, // 11: objdiff.report.ChangeUnit.functions:type_name -> objdiff.report.ChangeItem + 8, // 12: objdiff.report.ChangeItem.from:type_name -> objdiff.report.ChangeItemInfo + 8, // 13: objdiff.report.ChangeItem.to:type_name -> objdiff.report.ChangeItemInfo + 14, // [14:14] is the sub-list for method output_type + 14, // [14:14] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name +} + +func init() { file_report_proto_init() } +func file_report_proto_init() { + if File_report_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_report_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*Report); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_report_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*ReportUnit); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_report_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*ReportItem); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_report_proto_msgTypes[3].Exporter = func(v any, i int) any { + switch v := v.(*ChangesInput); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_report_proto_msgTypes[4].Exporter = func(v any, i int) any { + switch v := v.(*Changes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_report_proto_msgTypes[5].Exporter = func(v any, i int) any { + switch v := v.(*ChangeInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_report_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*ChangeUnit); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_report_proto_msgTypes[7].Exporter = func(v any, i int) any { + switch v := v.(*ChangeItem); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_report_proto_msgTypes[8].Exporter = func(v any, i int) any { + switch v := v.(*ChangeItemInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_report_proto_msgTypes[1].OneofWrappers = []any{} + file_report_proto_msgTypes[2].OneofWrappers = []any{} + file_report_proto_msgTypes[6].OneofWrappers = []any{} + file_report_proto_msgTypes[7].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_report_proto_rawDesc, + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_report_proto_goTypes, + DependencyIndexes: file_report_proto_depIdxs, + MessageInfos: file_report_proto_msgTypes, + }.Build() + File_report_proto = out.File + file_report_proto_rawDesc = nil + file_report_proto_goTypes = nil + file_report_proto_depIdxs = nil +} diff --git a/zipstream/zipstream.go b/zipstream/zipstream.go new file mode 100644 index 0000000..4a23acb --- /dev/null +++ b/zipstream/zipstream.go @@ -0,0 +1,275 @@ +// Package zipstream +// A streaming zip reader that specifically supports GitHub Actions artifacts files. +// The standard library's `archive/zip` uses the central directory at the end of the file, +// so it would require downloading the entire file before reading it. +// This package reads the local file headers and supports DEFLATE streams with an unknown size, +// allowing us to read files as the response is being downloaded. +package zipstream + +import ( + "archive/zip" + "bufio" + "compress/flate" + "encoding/binary" + "fmt" + "io" +) + +const ( + headerIdentifierLen = 4 + fileHeaderLen = 26 + dataDescriptorLen = 16 // four uint32: descriptor signature, crc32, compressed size, size + fileHeaderSignature = 0x04034b50 + directoryHeaderSignature = 0x02014b50 + directoryEndSignature = 0x06054b50 + dataDescriptorSignature = 0x08074b50 + zip64ExtraID = 0x0001 // Zip64 extended information +) + +type Reader struct { + r io.Reader + rBuf *bufio.Reader + fr io.ReadCloser + localFileEnd bool + curEntry *Entry +} + +func NewReader(r io.Reader) *Reader { + return &Reader{ + r: r, + fr: nil, + } +} + +type Entry struct { + zip.FileHeader + r io.Reader +} + +func (e *Entry) hasDataDescriptor() bool { + return e.Flags&8 != 0 +} + +// IsDir just simply check whether the entry name ends with "/" +func (e *Entry) IsDir() bool { + return len(e.Name) > 0 && e.Name[len(e.Name)-1] == '/' +} + +func (e *Entry) Read(p []byte) (n int, err error) { + return e.r.Read(p) +} + +//goland:noinspection GoDeprecation +func (z *Reader) readEntry() (*Entry, error) { + buf := make([]byte, fileHeaderLen) + if _, err := io.ReadFull(z.r, buf); err != nil { + return nil, fmt.Errorf("unable to read local file header: %w", err) + } + + lr := readBuf(buf) + readerVersion := lr.uint16() + flags := lr.uint16() + method := lr.uint16() + modifiedTime := lr.uint16() + modifiedDate := lr.uint16() + crc32Sum := lr.uint32() + compressedSize := lr.uint32() + uncompressedSize := lr.uint32() + filenameLen := int(lr.uint16()) + extraAreaLen := int(lr.uint16()) + + entry := &Entry{ + FileHeader: zip.FileHeader{ + ReaderVersion: readerVersion, + Flags: flags, + Method: method, + ModifiedTime: modifiedTime, + ModifiedDate: modifiedDate, + CRC32: crc32Sum, + CompressedSize: compressedSize, + UncompressedSize: uncompressedSize, + CompressedSize64: uint64(compressedSize), + UncompressedSize64: uint64(uncompressedSize), + }, + r: nil, + } + + nameAndExtraBuf := make([]byte, filenameLen+extraAreaLen) + if _, err := io.ReadFull(z.r, nameAndExtraBuf); err != nil { + return nil, fmt.Errorf("unable to read entry name and extra area: %w", err) + } + + entry.Name = string(nameAndExtraBuf[:filenameLen]) + entry.Extra = nameAndExtraBuf[filenameLen:] + + entry.NonUTF8 = flags&0x800 == 0 + if flags&1 == 1 { + return nil, fmt.Errorf("encrypted ZIP entry not supported") + } + if flags&8 == 8 && method != zip.Deflate { + return nil, fmt.Errorf("only DEFLATED entries can have data descriptor") + } + + needCSize := entry.CompressedSize == ^uint32(0) + needUSize := entry.UncompressedSize == ^uint32(0) + + ler := readBuf(entry.Extra) + for len(ler) >= 4 { // need at least tag and size + fieldTag := ler.uint16() + fieldSize := int(ler.uint16()) + if len(ler) < fieldSize { + break + } + fieldBuf := ler.sub(fieldSize) + + switch fieldTag { + case zip64ExtraID: + // update directory values from the zip64 extra block. + // They should only be consulted if the sizes read earlier + // are maxed out. + // See golang.org/issue/13367. + if needUSize { + needUSize = false + if len(fieldBuf) < 8 { + return nil, zip.ErrFormat + } + entry.UncompressedSize64 = fieldBuf.uint64() + } + if needCSize { + needCSize = false + if len(fieldBuf) < 8 { + return nil, zip.ErrFormat + } + entry.CompressedSize64 = fieldBuf.uint64() + } + } + } + + if needCSize { + return nil, zip.ErrFormat + } + + if method == zip.Store { + entry.r = io.LimitReader(z.r, int64(entry.UncompressedSize64)) + } else if method == zip.Deflate { + var reader io.Reader + if entry.CompressedSize64 > 0 { + reader = io.LimitReader(z.r, int64(entry.CompressedSize64)) + } else { + // unknown size; read until deflate EOF, + // but we need z.r to be an io.ByteReader for flate to not overread + if _, ok := z.r.(io.ByteReader); !ok { + z.r = bufio.NewReader(z.r) + } + reader = z.r + } + if z.fr == nil { + z.fr = flate.NewReader(reader) + } else { + z.fr.(flate.Resetter).Reset(reader, nil) + } + entry.r = z.fr + } else { + return nil, fmt.Errorf("unknown compression method %d", method) + } + + return entry, nil +} + +func (z *Reader) Next() (*Entry, error) { + if z.localFileEnd { + return nil, io.EOF + } + if z.curEntry != nil { + // Read any remaining data for the current file, if necessary. + if _, err := io.Copy(io.Discard, z.curEntry); err != nil { + return nil, fmt.Errorf("read previous file data fail: %w", err) + } + // Read the data descriptor if present. + if z.curEntry.hasDataDescriptor() { + if err := readDataDescriptor(z.r); err != nil { + return nil, fmt.Errorf("read previous entry's data descriptor fail: %w", err) + } + } + } + headerIDBuf := make([]byte, headerIdentifierLen) + if _, err := io.ReadFull(z.r, headerIDBuf); err != nil { + return nil, fmt.Errorf("unable to read header identifier: %w", err) + } + headerID := binary.LittleEndian.Uint32(headerIDBuf) + if headerID != fileHeaderSignature { + if headerID == directoryHeaderSignature || headerID == directoryEndSignature { + z.localFileEnd = true + return nil, io.EOF + } + return nil, zip.ErrFormat + } + entry, err := z.readEntry() + if err != nil { + return nil, fmt.Errorf("unable to read zip file header: %w", err) + } + z.curEntry = entry + return entry, nil +} + +func readDataDescriptor(r io.Reader) error { + var buf [dataDescriptorLen]byte + // The spec says: "Although not originally assigned a + // signature, the value 0x08074b50 has commonly been adopted + // as a signature value for the data descriptor record. + // Implementers should be aware that ZIP files may be + // encountered with or without this signature marking data + // descriptors and should account for either case when reading + // ZIP files to ensure compatibility." + // + // dataDescriptorLen includes the size of the signature but + // first read just those 4 bytes to see if it exists. + _, err := io.ReadFull(r, buf[:4]) + if err != nil { + return err + } + off := 0 + maybeSig := readBuf(buf[:4]) + if maybeSig.uint32() != dataDescriptorSignature { + // No data descriptor signature. Keep these four bytes. + off += 4 + } + _, err = io.ReadFull(r, buf[off:12]) + if err != nil { + return err + } + + return nil +} + +type readBuf []byte + +func (b *readBuf) uint8() uint8 { + v := (*b)[0] + *b = (*b)[1:] + return v +} + +func (b *readBuf) uint16() uint16 { + v := binary.LittleEndian.Uint16(*b) + *b = (*b)[2:] + return v +} + +func (b *readBuf) uint32() uint32 { + v := binary.LittleEndian.Uint32(*b) + *b = (*b)[4:] + return v +} + +func (b *readBuf) uint64() uint64 { + v := binary.LittleEndian.Uint64(*b) + *b = (*b)[8:] + return v +} + +func (b *readBuf) sub(n int) readBuf { + b2 := (*b)[:n] + *b = (*b)[n:] + return b2 +}