You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
garble/reverse.go

194 lines
4.7 KiB
Go

// Copyright (c) 2019, The Garble Authors.
// See LICENSE for licensing information.
package main
import (
"bufio"
"flag"
"fmt"
"go/ast"
"go/parser"
"go/types"
"io"
"os"
"path/filepath"
"strings"
)
// commandReverse implements "garble reverse".
func commandReverse(args []string) error {
flags, args := splitFlagsFromArgs(args)
mainPkg := "."
if len(args) > 0 {
mainPkg = args[0]
args = args[1:]
}
listArgs := []string{
"-json",
"-deps",
"-export",
}
listArgs = append(listArgs, flags...)
listArgs = append(listArgs, mainPkg)
// TODO: We most likely no longer need this "list -toolexec" call, since
// we use the original build IDs.
if _, err := toolexecCmd("list", listArgs); err != nil {
return err
}
// We don't actually run a main Go command with all flags,
// so if the user gave a non-build flag,
// we need this check to not silently ignore it.
if _, firstUnknown := filterBuildFlags(flags); firstUnknown != "" {
// A bit of a hack to get a normal flag.Parse error.
// Longer term, "reverse" might have its own FlagSet.
return flag.NewFlagSet("", flag.ContinueOnError).Parse([]string{firstUnknown})
}
// A package's names are generally hashed with the action ID of its
// obfuscated build. We recorded those action IDs above.
// Note that we parse Go files directly to obtain the names, since the
// export data only exposes exported names. Parsing Go files is cheap,
// so it's unnecessary to try to avoid this cost.
var replaces []string
for _, lpkg := range cache.ListedPackages {
if !lpkg.Private {
continue
}
curPkg = lpkg
addReplace := func(hash []byte, str string) {
if hash == nil {
hash = lpkg.GarbleActionID
}
replaces = append(replaces, hashWith(hash, str), str)
}
// Package paths are obfuscated, too.
addReplace(nil, lpkg.ImportPath)
var files []*ast.File
for _, goFile := range lpkg.GoFiles {
fullGoFile := filepath.Join(lpkg.Dir, goFile)
file, err := parser.ParseFile(fset, fullGoFile, nil, 0)
if err != nil {
return err
}
files = append(files, file)
}
tf := newTransformer()
if err := tf.typecheck(files); err != nil {
return err
}
for i, file := range files {
goFile := lpkg.GoFiles[i]
ast.Inspect(file, func(node ast.Node) bool {
switch node := node.(type) {
// Replace names.
// TODO: do var names ever show up in output?
case *ast.FuncDecl:
addReplace(nil, node.Name.Name)
case *ast.TypeSpec:
addReplace(nil, node.Name.Name)
case *ast.Field:
for _, name := range node.Names {
obj, _ := tf.info.ObjectOf(name).(*types.Var)
if obj == nil || !obj.IsField() {
continue
}
strct := tf.fieldToStruct[obj]
if strct == nil {
panic("could not find for " + name.Name)
}
fieldsHash := []byte(strct.String())
hashToUse := addGarbleToHash(fieldsHash)
addReplace(hashToUse, name.Name)
}
case *ast.CallExpr:
// continues below
default:
return true
}
// Reverse position information.
pos := fset.Position(node.Pos())
origPos := fmt.Sprintf("%s:%d", goFile, pos.Offset)
newPos := hashWith(lpkg.GarbleActionID, origPos) + ".go:1"
replaces = append(replaces,
newPos,
fmt.Sprintf("%s/%s:%d", lpkg.ImportPath, goFile, pos.Line),
)
return true
})
}
}
repl := strings.NewReplacer(replaces...)
if len(args) == 0 {
modified, err := reverseContent(os.Stdout, os.Stdin, repl)
if err != nil {
return err
}
if !modified {
return errJustExit
}
return nil
}
anyModified := false
for _, path := range args {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
any, err := reverseContent(os.Stdout, f, repl)
if err != nil {
return err
}
anyModified = anyModified || any
f.Close() // since we're in a loop
}
if !anyModified {
return errJustExit
}
return nil
}
func reverseContent(w io.Writer, r io.Reader, repl *strings.Replacer) (bool, error) {
// Read line by line.
// Reading the entire content at once wouldn't be interactive,
// nor would it support large files well.
// Reading entire lines ensures we don't cut words in half.
// We use bufio.Reader instead of bufio.Scanner,
// to also obtain the newline characters themselves.
br := bufio.NewReader(r)
modified := false
for {
// Note that ReadString can return a line as well as an error if
// we hit EOF without a newline.
// In that case, we still want to process the string.
line, readErr := br.ReadString('\n')
newLine := repl.Replace(line)
if newLine != line {
modified = true
}
if _, err := io.WriteString(w, newLine); err != nil {
return modified, err
}
if readErr == io.EOF {
return modified, nil
}
if readErr != nil {
return modified, readErr
}
}
}