|
package main |
|
|
|
import ( |
|
"bufio" |
|
"encoding/json" |
|
"flag" |
|
"fmt" |
|
"log" |
|
"os" |
|
"path/filepath" |
|
"sort" |
|
"strings" |
|
) |
|
|
|
const WahooPackageName = "com/wahoofitness" |
|
|
|
type ClassInfo struct { |
|
Name string |
|
Rename string `json:",omitempty"` |
|
Fields []ClassField |
|
Methods []ClassMethod |
|
} |
|
|
|
type ClassField struct { |
|
AccessFlags string `json:",omitempty"` |
|
Name string |
|
Rename string `json:",omitempty"` |
|
Type string |
|
Value string `json:",omitempty"` |
|
} |
|
|
|
type ClassMethod struct { |
|
AccessFlags string `json:",omitempty"` |
|
Name string |
|
Rename string `json:",omitempty"` |
|
Parameters string `json:",omitempty"` |
|
ReturnType string |
|
} |
|
|
|
func main() { |
|
var ( |
|
prettyJson bool |
|
writeJson bool |
|
writeClassMap bool |
|
inputDir string |
|
outputFile string |
|
) |
|
|
|
flag.BoolVar(&prettyJson, "p", false, "write pretty json. results in a bigger output file") |
|
flag.BoolVar(&writeJson, "out-json", false, "write output to .json") |
|
flag.BoolVar(&writeClassMap, "out-class", false, "write class map to .txt") |
|
flag.StringVar(&inputDir, "i", "", "apktool output directory") |
|
flag.StringVar(&outputFile, "o", "classdump", "output file to write") |
|
flag.Parse() |
|
|
|
if inputDir == "" { |
|
flag.Usage() |
|
os.Exit(1) |
|
} |
|
|
|
files, err := FindFiles(inputDir) |
|
if err != nil { |
|
log.Fatal(err) |
|
} |
|
|
|
classes := make(map[string]ClassInfo, len(files)) |
|
|
|
innerClasses := make([]string, 0) |
|
for _, filename := range files { |
|
if strings.Contains(filename, "$") { |
|
|
|
innerClasses = append(innerClasses, filename) |
|
continue |
|
} |
|
|
|
className := ClassFromFilename(filename) |
|
classInfo := AnalyzeFile(filename) |
|
classInfo.Name = className |
|
|
|
classes[className] = classInfo |
|
} |
|
|
|
if writeJson { |
|
const ext = ".json" |
|
if !strings.HasSuffix(outputFile, ext) { |
|
outputFile += ext |
|
} |
|
if err := WriteJson(outputFile, classes, prettyJson); err != nil { |
|
log.Fatal(err) |
|
} |
|
} |
|
if writeClassMap { |
|
const ext = ".txt" |
|
if !strings.HasSuffix(outputFile, ext) { |
|
outputFile += ext |
|
} |
|
if err := WriteClassMap(outputFile, classes); err != nil { |
|
log.Fatal(err) |
|
} |
|
} |
|
|
|
fmt.Printf("Skipped %d inner class files\n", len(innerClasses)) |
|
fmt.Printf("Total %d files\n", len(files)) |
|
|
|
// innerMatch := 0 |
|
// for _, v := range innerClasses { |
|
// className := ClassFromFilename(v) |
|
// dollarIdx := strings.Index(className, "$") |
|
// // fmt.Printf("%#v\n", className[:dollarIdx]) |
|
// if v, ok := classes[className[:dollarIdx-1]]; ok { |
|
// _ = v |
|
// innerMatch++ |
|
// if v.Rename != "" { |
|
// fmt.Printf("%s -> %s%s\n", v.Name, v.Rename, className[dollarIdx:]) |
|
// // } else { |
|
// // fmt.Printf("%s\n", v.Name) |
|
// } |
|
|
|
// } |
|
// } |
|
fmt.Println("Done") |
|
} |
|
|
|
func WriteClassMap(outputFile string, classes map[string]ClassInfo) error { |
|
fmt.Printf("Writing %d classes to %s\n", len(classes), outputFile) |
|
|
|
sorted := make([]string, 0, len(classes)) |
|
for k := range classes { |
|
sorted = append(sorted, k) |
|
} |
|
|
|
sort.Strings(sorted) |
|
|
|
for _, cName := range sorted { |
|
v := classes[cName] |
|
if v.Rename != "" { |
|
fmt.Printf("%s -> %s\n", v.Name, v.Rename) |
|
} else { |
|
fmt.Printf("%s -> \n", v.Name) |
|
} |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func WriteJson(outputFile string, classes map[string]ClassInfo, pretty bool) error { |
|
fmt.Printf("Writing %d classes to %s\n", len(classes), outputFile) |
|
f, err := os.Create(outputFile) |
|
if err != nil { |
|
log.Fatal(err) |
|
} |
|
defer f.Close() |
|
|
|
if pretty { |
|
buf, err := json.MarshalIndent(classes, "", " ") |
|
if err != nil { |
|
log.Fatal(err) |
|
} |
|
f.Write(buf) |
|
} else { |
|
if err := json.NewEncoder(f).Encode(classes); err != nil { |
|
log.Fatal(err) |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
func AnalyzeFile(filename string) ClassInfo { |
|
f, err := os.Open(filename) |
|
if err != nil { |
|
log.Fatal(err) |
|
} |
|
defer f.Close() |
|
|
|
s := bufio.NewScanner(f) |
|
|
|
fields := make([]ClassField, 0) |
|
methods := make([]ClassMethod, 0) |
|
|
|
for s.Scan() { |
|
line := s.Text() |
|
|
|
if strings.HasPrefix(line, ".field") { |
|
var classField ClassField |
|
field := line[7:] |
|
|
|
if strings.Contains(field, " = ") { |
|
split := strings.Split(field, " = ") |
|
classField.Value = split[1] |
|
field = split[0] |
|
} |
|
split := strings.Split(field, " ") |
|
|
|
ntIdx := len(split) - 1 |
|
if ntIdx > 0 { |
|
classField.AccessFlags = strings.Join(split[:ntIdx], " ") |
|
} |
|
|
|
nt := strings.Split(split[ntIdx], ":") |
|
classField.Name = nt[0] |
|
classField.Type = nt[1] |
|
|
|
fields = append(fields, classField) |
|
|
|
} else if strings.HasPrefix(line, ".method") { |
|
var classMethod ClassMethod |
|
method := line[8:] |
|
|
|
pStart := strings.Index(method, "(") |
|
pEnd := strings.Index(method, ")") |
|
classMethod.Parameters = method[pStart+1 : pEnd] |
|
classMethod.ReturnType = method[pEnd+1:] |
|
|
|
split := strings.Split(method[:pStart], " ") |
|
|
|
nIdx := len(split) - 1 |
|
if nIdx > 0 { |
|
classMethod.AccessFlags = strings.Join(split[:nIdx], " ") |
|
} |
|
|
|
classMethod.Name = split[nIdx] |
|
|
|
methods = append(methods, classMethod) |
|
} |
|
} |
|
|
|
ci := ClassInfo{Fields: fields, Methods: methods} |
|
className := ClassFromFilename(filename) |
|
ci.Name = className |
|
ci.findClassname() |
|
|
|
return ci |
|
} |
|
|
|
func (ci *ClassInfo) findClassname() { |
|
pkgIdx := strings.LastIndex(ci.Name, "/") |
|
|
|
if len(ci.Fields) > 0 { |
|
nameFields := make([]ClassField, 0) |
|
for _, field := range ci.Fields { |
|
if field.AccessFlags == "private static final" && field.Type == "Ljava/lang/String;" { |
|
if field.Value != "" { |
|
nameFields = append(nameFields, field) |
|
} |
|
} |
|
} |
|
|
|
switch len(nameFields) { |
|
case 0: |
|
return |
|
case 1: |
|
name, ok := ValidClassname(nameFields[0].Value) |
|
if ok { |
|
if ci.Name[:pkgIdx+1]+name != ci.Name { |
|
ci.Rename = ci.Name[:pkgIdx+1] + name |
|
} |
|
} |
|
default: |
|
name, ok := ValidClassField(nameFields) |
|
if ok { |
|
if ci.Name[:pkgIdx+1]+name != ci.Name { |
|
ci.Rename = ci.Name[:pkgIdx+1] + name |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
func FindFiles(dir string) ([]string, error) { |
|
files := []string{} |
|
|
|
err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { |
|
if strings.Contains(path, WahooPackageName) && filepath.Ext(path) == ".smali" { |
|
files = append(files, path) |
|
} |
|
return nil |
|
}) |
|
|
|
return files, err |
|
} |
|
|
|
func ClassFromFilename(filename string) string { |
|
pkgIdx := strings.Index(filename, WahooPackageName) |
|
extIdx := strings.Index(filename, ".smali") |
|
return filename[pkgIdx:extIdx] |
|
} |
|
|
|
func ValidClassField(fields []ClassField) (string, bool) { |
|
names := make(map[string]struct{}) |
|
|
|
for _, field := range fields { |
|
name, ok := ValidClassname(field.Value) |
|
if !ok { |
|
continue |
|
} |
|
if _, ok := names[name]; !ok { |
|
names[name] = struct{}{} |
|
break |
|
} |
|
} |
|
|
|
if len(names) == 0 { |
|
return "", false |
|
} else if len(names) == 1 { |
|
for k := range names { |
|
return k, true |
|
} |
|
} |
|
|
|
log.Fatalf("not expected: %#v\n", names) |
|
return "", false |
|
} |
|
|
|
func ValidClassname(s string) (string, bool) { |
|
if strings.HasPrefix(s, "\"") && strings.HasSuffix(s, "\"") { |
|
s = s[1 : len(s)-1] |
|
} |
|
|
|
if strings.Contains(s, ".") { |
|
return "", false |
|
} |
|
|
|
if strings.HasSuffix(s, "Manager") { |
|
return s, true |
|
} |
|
if strings.HasPrefix(s, "Std") { |
|
return s, true |
|
} |
|
|
|
if strings.Contains(s, "#") { |
|
return "", false |
|
} else if strings.Contains(s, "-") { |
|
return "", false |
|
} else if strings.HasPrefix(s, "BA") { |
|
return s, true |
|
} else if strings.HasPrefix(s, "B") { |
|
return s, true |
|
} else if s[0] < 'A' || s[0] > 'Z' { |
|
return "", false |
|
} |
|
|
|
if strings.Title(s) == s { |
|
return "", false |
|
} |
|
|
|
cCount := strings.Count(s, "_") |
|
if cCount == 0 { |
|
return s, true |
|
} else if cCount == 1 { |
|
return s, true |
|
} else if cCount > 1 { |
|
return "", false |
|
} |
|
|
|
log.Printf("what: %s\n", s) |
|
|
|
return "", false |
|
} |