mirror of
https://github.com/encounter/decomp.dev.git
synced 2026-07-10 03:18:48 -07:00
Initial commit
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user