Initial commit

This commit is contained in:
Luke Street
2024-08-16 00:41:38 -06:00
commit 84bb5c1946
16 changed files with 2676 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
/tmp
/.idea
objdiff-*
config.yml
+101
View File
@@ -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")
}
}
+25
View File
@@ -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
+36
View File
@@ -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
}
Executable
+6
View File
@@ -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
+34
View File
@@ -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
)
+75
View File
@@ -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=
+123
View File
@@ -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
}
+387
View File
@@ -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(
"<details><summary>Version %s</summary>\n\n%s\n\n</details>\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),
)
}
+22
View File
@@ -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")
}
}
+109
View File
@@ -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
}
+65
View File
@@ -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
}
+109
View File
@@ -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,
}
}
+153
View File
@@ -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<version>[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
}
+1152
View File
File diff suppressed because it is too large Load Diff
+275
View File
@@ -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
}