From 47b08df1453f56c3af62c4f0f5a0091603081c0f Mon Sep 17 00:00:00 2001 From: superp00t Date: Sat, 28 Feb 2026 02:34:20 -0500 Subject: [PATCH] feat(binana): change tool to deposit generated files into an 'artifacts' folder that isn't retained by repository history --- .github/workflows/push.yml | 42 +++++ .gitignore | 4 +- Makefile | 11 +- ghidra/ExportSymbolsScript.py | 70 ++++----- go.mod | 8 +- go.sum | 6 - go/app/app.go | 11 ++ go/app/cmd/execute.go | 17 ++ go/app/cmd/lint/lint.go | 43 ++++++ go/app/cmd/make/make.go | 32 ++++ go/app/cmd/root/root.go | 8 + go/app/cmd/tidy/tidy.go | 25 +++ go/app/profile/lint.go | 65 ++++++++ go/app/profile/make.go | 20 +++ go/app/profile/profile.go | 26 ++++ go/app/profile/tidy.go | 61 ++++++++ go/cmd/binana/cmd/check.go | 97 ------------ go/cmd/binana/cmd/format_symbols.go | 92 ----------- go/cmd/binana/cmd/generate.go | 37 ----- go/cmd/binana/cmd/root.go | 38 ----- go/cmd/binana/cmd/x64dbg_typesort.go | 32 ---- go/cmd/binana/main.go | 7 - go/cmd/bna/main.go | 7 + go/profile/compile_artifacts.go | 35 +++++ go/profile/compile_ghidra_artifacts.go | 21 +++ ...e_files.go => compile_idapro_artifacts.go} | 71 ++++++--- go/profile/compile_x64dbg_artifacts.go | 9 ++ ...database.go => compile_x64dbg_database.go} | 26 ++-- ...erate_types.go => compile_x64dbg_types.go} | 9 +- go/profile/generate.go | 15 -- go/profile/ida/import_all.idc | 13 ++ go/profile/ida/import_data_types.idc | 6 + go/profile/ida/import_functions.idc | 5 + go/profile/ida/import_symbols.idc | 5 + go/profile/open.go | 36 ++--- go/profile/x64dbg.go | 13 -- go/symbols/error.go | 8 + go/symbols/loader.go | 79 ++++++++++ .../parse_symbol.go} | 22 +-- go/symbols/symbol.go | 77 ++++++++++ go/symbols/table.go | 145 ++++++++++++++++++ go/symfile/loader.go | 70 --------- go/symfile/memory_table.go | 38 ----- go/symfile/symfile.go | 64 -------- 44 files changed, 904 insertions(+), 622 deletions(-) create mode 100644 .github/workflows/push.yml create mode 100644 go/app/app.go create mode 100644 go/app/cmd/execute.go create mode 100644 go/app/cmd/lint/lint.go create mode 100644 go/app/cmd/make/make.go create mode 100644 go/app/cmd/root/root.go create mode 100644 go/app/cmd/tidy/tidy.go create mode 100644 go/app/profile/lint.go create mode 100644 go/app/profile/make.go create mode 100644 go/app/profile/profile.go create mode 100644 go/app/profile/tidy.go delete mode 100644 go/cmd/binana/cmd/check.go delete mode 100644 go/cmd/binana/cmd/format_symbols.go delete mode 100644 go/cmd/binana/cmd/generate.go delete mode 100644 go/cmd/binana/cmd/root.go delete mode 100644 go/cmd/binana/cmd/x64dbg_typesort.go delete mode 100644 go/cmd/binana/main.go create mode 100644 go/cmd/bna/main.go create mode 100644 go/profile/compile_artifacts.go create mode 100644 go/profile/compile_ghidra_artifacts.go rename go/profile/{ida_generate_files.go => compile_idapro_artifacts.go} (58%) create mode 100644 go/profile/compile_x64dbg_artifacts.go rename go/profile/{x64dbg_generate_database.go => compile_x64dbg_database.go} (65%) rename go/profile/{x64dbg_generate_types.go => compile_x64dbg_types.go} (95%) delete mode 100644 go/profile/generate.go create mode 100644 go/profile/ida/import_all.idc create mode 100644 go/profile/ida/import_data_types.idc create mode 100644 go/profile/ida/import_functions.idc create mode 100644 go/profile/ida/import_symbols.idc delete mode 100644 go/profile/x64dbg.go create mode 100644 go/symbols/error.go create mode 100644 go/symbols/loader.go rename go/{symfile/parse_entry.go => symbols/parse_symbol.go} (85%) create mode 100644 go/symbols/symbol.go create mode 100644 go/symbols/table.go delete mode 100644 go/symfile/loader.go delete mode 100644 go/symfile/memory_table.go delete mode 100644 go/symfile/symfile.go diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..4f6e6bf --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,42 @@ +name: Push + +on: + push: + paths: + - "Makefile" + - "go.mod" + - "go.sum" + - "go/**" + - "profile/**" + branches: + - artifacts + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: + "stable" # Uses the latest stable Go version + + # checkout repository + - name: Checkout code + uses: actions/checkout@v6 + + # build binana tool + - name: Make tool + run: make dependencies + + # run binana tool + - name: Make artifacts + run: make artifacts + + # upload artifacts + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: artifacts + path: artifacts/ diff --git a/.gitignore b/.gitignore index 232df63..5979075 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ jest .vscode -bin \ No newline at end of file +bin +artifacts +profile/*/symbol/main.sym \ No newline at end of file diff --git a/Makefile b/Makefile index c834437..2d54e04 100644 --- a/Makefile +++ b/Makefile @@ -3,14 +3,11 @@ all: generate ce-lua: ./script/build-cheatengine-scripts cheatengine profile/3.3.5a-windows-386/cheatengine -compile-symbols: - ./script/compile-symbols profile/3.3.5a-windows-386 +artifacts: + ./bin/bna mk 3.3.5a-windows-386 -profile-gen: - ./bin/binana generate --profile profile/3.3.5a-windows-386 - -generate: compile-symbols profile-gen +generate: artifacts dependencies: mkdir -p bin - go build -v -o bin/binana github.com/thunderbrewhq/binana/go/cmd/binana + go build -v -o bin/bna github.com/thunderbrewhq/binana/go/cmd/bna diff --git a/ghidra/ExportSymbolsScript.py b/ghidra/ExportSymbolsScript.py index 6eb9496..6822117 100644 --- a/ghidra/ExportSymbolsScript.py +++ b/ghidra/ExportSymbolsScript.py @@ -1,36 +1,36 @@ -from ghidra.program.model.symbol.SourceType import * - -functionManager = currentProgram.getFunctionManager() - -file_location = askFile("Choose a file to write to", "Go baby go!") - -listing = currentProgram.getListing() - -def export_function_symbols(file): - monitor.setMessage("Exporting function symbols...") - - for f in functionManager.getFunctionsNoStubs(1): - monitor.checkCanceled() # throws exception if canceled - - if f.isExternal() or f.isThunk(): - continue - - func_name = f.getName() - - if func_name.startswith("FUN_"): - continue - - func_start_address = f.getBody().getMinAddress().getOffset() - func_end_address = f.getBody().getMaxAddress().getOffset() + 1 - - line_template = "{name} {start_address:08X} f end={end_address:08X}\n" - - func_line = line_template.format(name = func_name, start_address = func_start_address, end_address = func_end_address) - - file.write(func_line) - - return - -with open(file_location.absolutePath, "w") as file: - export_function_symbols(file) +from ghidra.program.model.symbol.SourceType import * + +functionManager = currentProgram.getFunctionManager() + +file_location = askFile("Choose a file to write to", "Go baby go!") + +listing = currentProgram.getListing() + +def export_function_symbols(file): + monitor.setMessage("Exporting function symbols...") + + for f in functionManager.getFunctionsNoStubs(1): + monitor.checkCanceled() # throws exception if canceled + + if f.isExternal() or f.isThunk(): + continue + + func_name = f.getName() + + if func_name.startswith("FUN_"): + continue + + func_start_address = f.getBody().getMinAddress().getOffset() + func_end_address = f.getBody().getMaxAddress().getOffset() + 1 + + line_template = "{name} {start_address:08X} f end={end_address:08X}\n" + + func_line = line_template.format(name = func_name, start_address = func_start_address, end_address = func_end_address) + + file.write(func_line) + + return + +with open(file_location.absolutePath, "w") as file: + export_function_symbols(file) file.close() \ No newline at end of file diff --git a/go.mod b/go.mod index 11f8411..52fec35 100644 --- a/go.mod +++ b/go.mod @@ -1,20 +1,18 @@ module github.com/thunderbrewhq/binana -go 1.22.0 +go 1.25.5 require ( + github.com/fatih/color v1.18.0 + github.com/pierrec/lz4/v4 v4.1.21 github.com/spf13/cobra v1.8.1 modernc.org/cc/v3 v3.41.0 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sys v0.25.0 // indirect diff --git a/go.sum b/go.sum index a0834e6..de33cfe 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd h1:EVX1s+XNss9jkRW9K6XGJn2jL2lB1h5H804oKPsxOec= -github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -16,8 +12,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= diff --git a/go/app/app.go b/go/app/app.go new file mode 100644 index 0000000..d73e74c --- /dev/null +++ b/go/app/app.go @@ -0,0 +1,11 @@ +package app + +import ( + "fmt" + "os" +) + +func Fatal(args ...any) { + fmt.Fprintln(os.Stderr, args...) + os.Exit(1) +} diff --git a/go/app/cmd/execute.go b/go/app/cmd/execute.go new file mode 100644 index 0000000..0e62ad9 --- /dev/null +++ b/go/app/cmd/execute.go @@ -0,0 +1,17 @@ +package cmd + +import ( + _ "github.com/thunderbrewhq/binana/go/app/cmd/lint" + _ "github.com/thunderbrewhq/binana/go/app/cmd/make" + _ "github.com/thunderbrewhq/binana/go/app/cmd/tidy" + + "github.com/thunderbrewhq/binana/go/app" + "github.com/thunderbrewhq/binana/go/app/cmd/root" +) + +func Execute() { + err := root.RootCmd.Execute() + if err != nil { + app.Fatal(err) + } +} diff --git a/go/app/cmd/lint/lint.go b/go/app/cmd/lint/lint.go new file mode 100644 index 0000000..3e0a182 --- /dev/null +++ b/go/app/cmd/lint/lint.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/thunderbrewhq/binana/go/app" + "github.com/thunderbrewhq/binana/go/app/profile" +) + +var lint_cmd = cobra.Command{ + Use: "lint profile", + Short: "show warnings and coverage for a profile", + Run: lint_func, +} + +func init() { + f := lint_cmd.Flags() + f.Bool("bounds", false, "check for bad function boundaries") + f.Bool("constructors", false, "check for outdated class constructor names") +} + +func lint_func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + cmd.Help() + return + } + f := cmd.Flags() + var params profile.LintParams + params.Profile = args[0] + + var err error + + params.Constructors, err = f.GetBool("constructors") + if err != nil { + app.Fatal(err) + } + + params.Bounds, err = f.GetBool("bounds") + if err != nil { + app.Fatal(err) + } + + profile.Lint(¶ms) +} diff --git a/go/app/cmd/make/make.go b/go/app/cmd/make/make.go new file mode 100644 index 0000000..bbc7b34 --- /dev/null +++ b/go/app/cmd/make/make.go @@ -0,0 +1,32 @@ +package make + +import ( + "github.com/spf13/cobra" + "github.com/thunderbrewhq/binana/go/app/cmd/root" + "github.com/thunderbrewhq/binana/go/app/profile" +) + +func mk_func(cmd *cobra.Command, args []string) { + compress, err := cmd.Flags().GetBool("compress") + if err != nil { + panic(err) + } + + var params profile.MakeParams + params.Profile = args[0] + params.CompressX64dbgDatabase = compress + profile.Make(¶ms) +} + +var mk_cmd = cobra.Command{ + Use: "mk profile", + Args: cobra.MinimumNArgs(1), + Short: "Convert source files into various tool formats", + Run: mk_func, +} + +func init() { + f := mk_cmd.Flags() + f.BoolP("compress", "c", true, "enable/disable compression of the x64dbg database file") + root.RootCmd.AddCommand(&mk_cmd) +} diff --git a/go/app/cmd/root/root.go b/go/app/cmd/root/root.go new file mode 100644 index 0000000..89ef02e --- /dev/null +++ b/go/app/cmd/root/root.go @@ -0,0 +1,8 @@ +package root + +import "github.com/spf13/cobra" + +var RootCmd = cobra.Command{ + Use: "bna", + Short: "Binana helper tool", +} diff --git a/go/app/cmd/tidy/tidy.go b/go/app/cmd/tidy/tidy.go new file mode 100644 index 0000000..7fb500d --- /dev/null +++ b/go/app/cmd/tidy/tidy.go @@ -0,0 +1,25 @@ +package tidy + +import ( + "github.com/spf13/cobra" + "github.com/thunderbrewhq/binana/go/app/cmd/root" + "github.com/thunderbrewhq/binana/go/app/profile" +) + +var tidy_cmd = cobra.Command{ + Use: "tidy profile", + Short: "sort all symbol files in a profile", + Args: cobra.MinimumNArgs(1), + Run: tidy_func, +} + +func init() { + //f := tidy_cmd.Flags() + root.RootCmd.AddCommand(&tidy_cmd) +} + +func tidy_func(cmd *cobra.Command, args []string) { + var params profile.TidyParams + params.Profile = args[0] + profile.Tidy(¶ms) +} diff --git a/go/app/profile/lint.go b/go/app/profile/lint.go new file mode 100644 index 0000000..c69745d --- /dev/null +++ b/go/app/profile/lint.go @@ -0,0 +1,65 @@ +package profile + +import ( + "fmt" + "strings" + + "github.com/fatih/color" + "github.com/thunderbrewhq/binana/go/symbols" +) + +type linter struct { + warnings uint64 + named_functions_count uint64 +} + +func (linter *linter) warn(s *symbols.TableEntry, f string, args ...any) { + linter.warnings++ + color.Set(color.FgRed) + fmt.Printf(" warning: ") + color.Unset() + fmt.Printf(" in %s:%d: %s", s.Filename, s.Linenumber, s.Symbol.Name) + fmt.Printf(f, args...) +} + +type LintParams struct { + Profile string + Constructors bool + Bounds bool +} + +func Lint(params *LintParams) { + Open(params.Profile) + defer Close() + + var linter linter + for entry := range Profile.Symbols.Entries() { + sn := entry.Symbol.Name + + if entry.Symbol.Kind == symbols.Function { + linter.named_functions_count++ + + if params.Constructors { + // + b, a, found := strings.Cut(sn, "__") + if found { + if b == a { + linter.warn(entry, "this style of naming a constructor function is preferred: ClassName__constructor\n") + } + } + } + + if params.Bounds { + if entry.Symbol.EndAddress == 0 { + linter.warn(entry, "does not have an end address\n") + } + } + } + } + + if Profile.Info.FunctionCount != 0 { + ratio := float64(linter.named_functions_count) / float64(Profile.Info.FunctionCount) + fmt.Printf("%d out of %d functions named (%f%%)\n", linter.named_functions_count, Profile.Info.FunctionCount, ratio*100.0) + fmt.Printf("%d warnings generated\n", linter.warnings) + } +} diff --git a/go/app/profile/make.go b/go/app/profile/make.go new file mode 100644 index 0000000..d3e26c6 --- /dev/null +++ b/go/app/profile/make.go @@ -0,0 +1,20 @@ +package profile + +import ( + "github.com/thunderbrewhq/binana/go/app" + "github.com/thunderbrewhq/binana/go/profile" +) + +type MakeParams struct { + Profile string + profile.CompileArtifactsParams +} + +func Make(params *MakeParams) { + Open(params.Profile) + defer Close() + + if err := Profile.CompileArtifacts(¶ms.CompileArtifactsParams); err != nil { + app.Fatal(err) + } +} diff --git a/go/app/profile/profile.go b/go/app/profile/profile.go new file mode 100644 index 0000000..6e53174 --- /dev/null +++ b/go/app/profile/profile.go @@ -0,0 +1,26 @@ +package profile + +import ( + "path/filepath" + + "github.com/thunderbrewhq/binana/go/app" + "github.com/thunderbrewhq/binana/go/profile" +) + +var Profile profile.Profile + +func Open(profile_name string) { + if profile_name == "" { + app.Fatal("no profile selected") + } + if err := Profile.Open( + filepath.Join("profile", profile_name), + filepath.Join("artifacts", profile_name), + ); err != nil { + app.Fatal(err) + } +} + +func Close() { + //Profile.Close() +} diff --git a/go/app/profile/tidy.go b/go/app/profile/tidy.go new file mode 100644 index 0000000..89be0ef --- /dev/null +++ b/go/app/profile/tidy.go @@ -0,0 +1,61 @@ +package profile + +import ( + "os" + "path/filepath" + + "github.com/thunderbrewhq/binana/go/app" + "github.com/thunderbrewhq/binana/go/symbols" +) + +type TidyParams struct { + Profile string +} + +func tidy_symbol_file(name string, params *TidyParams) (err error) { + var symbol_table symbols.Table + symbol_table.Init() + if err = symbol_table.Load(name); err != nil { + return + } + var f *os.File + f, err = os.Create(name) + if err != nil { + return + } + if _, err = symbol_table.WriteTo(f); err != nil { + f.Close() + return + } + err = f.Close() + return +} + +func tidy_symbol_directory(name string, params *TidyParams) (err error) { + var contents []os.DirEntry + contents, err = os.ReadDir(name) + if err != nil { + return + } + for _, content := range contents { + if content.IsDir() { + if err = tidy_symbol_directory(filepath.Join(name, content.Name()), params); err != nil { + return + } + } else { + if err = tidy_symbol_file(filepath.Join(name, content.Name()), params); err != nil { + return + } + } + } + return +} + +func Tidy(params *TidyParams) { + // tidy symbols + profile_symbols := filepath.Join("profile", params.Profile, "symbol") + + if err := tidy_symbol_directory(profile_symbols, params); err != nil { + app.Fatal(err) + } +} diff --git a/go/cmd/binana/cmd/check.go b/go/cmd/binana/cmd/check.go deleted file mode 100644 index de5a54a..0000000 --- a/go/cmd/binana/cmd/check.go +++ /dev/null @@ -1,97 +0,0 @@ -package cmd - -import ( - "fmt" - "strings" - - "github.com/fatih/color" - "github.com/spf13/cobra" - "github.com/thunderbrewhq/binana/go/profile" - "github.com/thunderbrewhq/binana/go/symfile" -) - -type check_params struct { - ProfileName string - Constructors bool - Bounds bool -} - -func check(params *check_params) { - p, err := profile.Open(params.ProfileName) - if err != nil { - panic(err) - } - - named_function_count := uint64(0) - - warnings := uint64(0) - - warn := func(s *symfile.Entry, f string, args ...any) { - warnings++ - color.Set(color.FgRed) - fmt.Printf(" warning: ") - color.Unset() - fmt.Printf("in line %d: (%s): ", s.LineNumber, s.Name) - fmt.Printf(f, args...) - } - - for _, s := range p.SymbolTable.Entries { - sn := s.Name - - if s.Kind == symfile.Function { - named_function_count++ - - if params.Constructors { - // - b, a, found := strings.Cut(sn, "__") - if found { - if b == a { - warn(&s, "this style of naming a constructor function is preferred: ClassName__constructor\n") - } - } - } - - if params.Bounds { - if s.EndAddress == 0 { - warn(&s, "does not have an end address\n") - } - } - } - } - - if p.Info.FunctionCount != 0 { - ratio := float64(named_function_count) / float64(p.Info.FunctionCount) - fmt.Printf("%d out of %d functions named (%f%%)\n", named_function_count, p.Info.FunctionCount, ratio*100.0) - fmt.Printf("%d warnings generated\n", warnings) - } -} - -func check_run(cmd *cobra.Command, args []string) { - if len(args) < 1 { - cmd.Help() - return - } - f := cmd.Flags() - var params check_params - params.ProfileName = args[0] - - var err error - - params.Constructors, err = f.GetBool("constructors") - if err != nil { - panic(err) - } - - params.Bounds, err = f.GetBool("bounds") - if err != nil { - panic(err) - } - - check(¶ms) -} - -var check_cmd = cobra.Command{ - Use: "check profile", - Short: "check a profile for correctness", - Run: check_run, -} diff --git a/go/cmd/binana/cmd/format_symbols.go b/go/cmd/binana/cmd/format_symbols.go deleted file mode 100644 index 0e101f6..0000000 --- a/go/cmd/binana/cmd/format_symbols.go +++ /dev/null @@ -1,92 +0,0 @@ -package cmd - -import ( - "bufio" - "fmt" - "io" - "os" - "strings" - - "github.com/ianlancetaylor/demangle" - "github.com/spf13/cobra" - "github.com/thunderbrewhq/binana/go/symfile" -) - -func demangle_symbol_name(name string) (bn_name string, dm_name string, type_str string, err error) { - // var ast demangle.AST - // ast, err = demangle.ToAST(name) - // if err != nil { - // bn_name = name - // err = nil - // return - // } - - // var name []string - // var args []string - - // ast.Traverse(func(a demangle.AST) bool { - // if fn_type, ok := a.(*demangle.FunctionType); ok { - // args = append(args, fn_) - // } - // }) - - var dm_err error - dm_name, dm_err = demangle.ToString(name) - if dm_err != nil { - bn_name = name - return - } - bn_name = dm_name - - if strings.Contains(bn_name, "(") { - var arg string - bn_name, arg, _ = strings.Cut(bn_name, "(") - type_str = fmt.Sprintf("uint32_t func(%s", arg) - } - - bn_name = strings.ReplaceAll(bn_name, "::", "__") - bn_name = strings.ReplaceAll(bn_name, "<", "_") - bn_name = strings.ReplaceAll(bn_name, ">", "") - - return -} - -func format_symbol_entry(entry *symfile.Entry, out io.Writer) { - outentry := *entry - - if outentry.Name[0] == '_' && outentry.Name[1] != 'Z' { - outentry.Name = outentry.Name[1:] - } - bn_name, dm_name, _, err := demangle_symbol_name(outentry.Name) - if err == nil { - outentry.Name = bn_name - outentry.Comment = dm_name - // if outentry.DataType == "" { - // outentry.DataType = dm_type - // } - } - out.Write([]byte(outentry.String())) - out.Write([]byte("\n")) -} - -func format_symbols(in io.Reader, out io.Writer) { - scanner := bufio.NewScanner(in) - for scanner.Scan() { - var entry symfile.Entry - if err := entry.Parse(scanner.Text()); err != nil { - fmt.Println(err) - os.Exit(1) - } - format_symbol_entry(&entry, out) - } -} - -func format_symbols_run(cmd *cobra.Command, args []string) { - format_symbols(os.Stdin, os.Stdout) -} - -var format_symbols_cmd = cobra.Command{ - Use: "format-symbols", - Short: "format symbols from stdin", - Run: format_symbols_run, -} diff --git a/go/cmd/binana/cmd/generate.go b/go/cmd/binana/cmd/generate.go deleted file mode 100644 index 7b25ef4..0000000 --- a/go/cmd/binana/cmd/generate.go +++ /dev/null @@ -1,37 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - "github.com/thunderbrewhq/binana/go/profile" -) - -func generate_run(cmd *cobra.Command, args []string) { - compress, err := cmd.Flags().GetBool("compress") - if err != nil { - panic(err) - } - - game_profile_directory, err := cmd.Flags().GetString("profile") - if err != nil { - panic(err) - } - - game_profile, err := profile.Open(game_profile_directory) - if err != nil { - panic(err) - } - - if err = game_profile.Generate(compress); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -var generate_cmd = cobra.Command{ - Use: "generate", - Short: "Convert source files into various tool formats", - Run: generate_run, -} diff --git a/go/cmd/binana/cmd/root.go b/go/cmd/binana/cmd/root.go deleted file mode 100644 index 22d24bb..0000000 --- a/go/cmd/binana/cmd/root.go +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package cmd - -import ( - "os" - - "github.com/spf13/cobra" -) - -// rootCmd represents the base command when called without any subcommands -var root = cobra.Command{ - Use: "binana", - Short: "Binana helper tool", -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := root.Execute() - if err != nil { - os.Exit(1) - } -} - -func init() { - generate_cmd.Flags().StringP("profile", "p", "3.3.5a", "the game profile") - generate_cmd.Flags().BoolP("compress", "c", true, "enable/disable compression of the x64dbg database file") - - check_cmd.Flags().Bool("bounds", false, "check for bad function boundaries") - check_cmd.Flags().Bool("constructors", false, "check for outdated class constructor names") - - root.AddCommand(&generate_cmd) - root.AddCommand(&format_symbols_cmd) - root.AddCommand(&x64dbg_typesort_cmd) - root.AddCommand(&check_cmd) -} diff --git a/go/cmd/binana/cmd/x64dbg_typesort.go b/go/cmd/binana/cmd/x64dbg_typesort.go deleted file mode 100644 index 01ee0e0..0000000 --- a/go/cmd/binana/cmd/x64dbg_typesort.go +++ /dev/null @@ -1,32 +0,0 @@ -package cmd - -import ( - "encoding/json" - "os" - - "github.com/spf13/cobra" - "github.com/thunderbrewhq/binana/go/x64dbg" -) - -var x64dbg_typesort_cmd = cobra.Command{ - Use: "x64dbg-typesort [types.json file]", - Short: "sort a x64dbg types file", - Run: x64dbg_typesort_run, -} - -func x64dbg_typesort_run(cmd *cobra.Command, args []string) { - types, err := x64dbg.LoadTypes(args[0]) - if err != nil { - panic(err) - } - - if err = x64dbg.SortTypes(types); err != nil { - panic(err) - } - - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - if err = encoder.Encode(types); err != nil { - panic(err) - } -} diff --git a/go/cmd/binana/main.go b/go/cmd/binana/main.go deleted file mode 100644 index 3bd3269..0000000 --- a/go/cmd/binana/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "github.com/thunderbrewhq/binana/go/cmd/binana/cmd" - -func main() { - cmd.Execute() -} diff --git a/go/cmd/bna/main.go b/go/cmd/bna/main.go new file mode 100644 index 0000000..b5e20fd --- /dev/null +++ b/go/cmd/bna/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/thunderbrewhq/binana/go/app/cmd" + +func main() { + cmd.Execute() +} diff --git a/go/profile/compile_artifacts.go b/go/profile/compile_artifacts.go new file mode 100644 index 0000000..b406bba --- /dev/null +++ b/go/profile/compile_artifacts.go @@ -0,0 +1,35 @@ +package profile + +import ( + "fmt" + "slices" +) + +type CompileArtifactsParams struct { + CompressX64dbgDatabase bool +} + +func (profile *Profile) CompileArtifacts(params *CompileArtifactsParams) (err error) { + if !profile.loaded { + panic(profile.loaded) + } + compilers := []struct { + OS []string + Name string + Fn func(*Profile, *CompileArtifactsParams) error + }{ + {[]string{"windows", "linux", "darwin"}, "Ghidra", compile_ghidra_artifacts}, + {[]string{"windows", "linux", "darwin"}, "IDA Pro", compile_idapro_artifacts}, + {[]string{"windows"}, "x64dbg", compile_x64dbg_artifacts}, + } + + for _, compiler := range compilers { + if slices.Contains(compiler.OS, profile.Info.OS) { + fmt.Println("compiling artifacts for", compiler.Name) + if err = compiler.Fn(profile, params); err != nil { + return + } + } + } + return +} diff --git a/go/profile/compile_ghidra_artifacts.go b/go/profile/compile_ghidra_artifacts.go new file mode 100644 index 0000000..f4eff37 --- /dev/null +++ b/go/profile/compile_ghidra_artifacts.go @@ -0,0 +1,21 @@ +package profile + +import ( + "os" + "path/filepath" +) + +func compile_ghidra_artifacts(profile *Profile, params *CompileArtifactsParams) (err error) { + ghidra_path := filepath.Join(profile.ArtifactsDirectory, "ghidra") + if err = os.MkdirAll(ghidra_path, 0755); err != nil { + return + } + var symbol_file *os.File + symbol_file, err = os.Create(filepath.Join(ghidra_path, "all.sym")) + if err != nil { + return + } + _, err = profile.Symbols.WriteTo(symbol_file) + symbol_file.Close() + return +} diff --git a/go/profile/ida_generate_files.go b/go/profile/compile_idapro_artifacts.go similarity index 58% rename from go/profile/ida_generate_files.go rename to go/profile/compile_idapro_artifacts.go index 92b8842..172aa02 100644 --- a/go/profile/ida_generate_files.go +++ b/go/profile/compile_idapro_artifacts.go @@ -1,12 +1,27 @@ package profile import ( + _ "embed" "fmt" "os" "path/filepath" "strconv" - "github.com/thunderbrewhq/binana/go/symfile" + "github.com/thunderbrewhq/binana/go/symbols" +) + +var ( + //go:embed ida/import_all.idc + import_all_script []byte + + //go:embed ida/import_data_types.idc + import_data_types_script []byte + + //go:embed ida/import_functions.idc + import_functions_script []byte + + //go:embed ida/import_symbols.idc + import_symbols_script []byte ) type idac_import_batch struct { @@ -64,8 +79,12 @@ func (b *idac_import_batch) Close() (err error) { } func (profile *Profile) create_idac_import_batch(name string) (b *idac_import_batch, err error) { + batch_path := filepath.Join(profile.ArtifactsDirectory, "ida", "batch") + if err = os.MkdirAll(batch_path, 0755); err != nil { + return + } b = new(idac_import_batch) - b.fd, err = os.Create(filepath.Join(profile.Directory, "ida", "batch", name+".idc")) + b.fd, err = os.Create(filepath.Join(batch_path, name+".idc")) if err != nil { return } @@ -73,8 +92,7 @@ func (profile *Profile) create_idac_import_batch(name string) (b *idac_import_ba return } -func (profile *Profile) generate_symbols_idc() (err error) { - // symbols +func compile_idapro_artifacts(profile *Profile, params *CompileArtifactsParams) (err error) { var b *idac_import_batch b, err = profile.create_idac_import_batch("import_symbols") if err != nil { @@ -88,8 +106,8 @@ func (profile *Profile) generate_symbols_idc() (err error) { name_instances := make(map[string]int) - for _, symbol := range profile.SymbolTable.Entries { - name := symbol.Name + for symbol := range profile.Symbols.Entries() { + name := symbol.Symbol.Name instances := name_instances[name] name_instances[name] = instances + 1 @@ -98,7 +116,7 @@ func (profile *Profile) generate_symbols_idc() (err error) { } quoted_name := strconv.Quote(name) - address := fmt.Sprintf("0x%08X", symbol.StartAddress) + address := fmt.Sprintf("0x%08X", symbol.Symbol.StartAddress) b.P("set_name(%s, %s);", address, quoted_name) } b.T(0) @@ -117,10 +135,10 @@ func (profile *Profile) generate_symbols_idc() (err error) { b.P("static import_data_types() {") b.T(1) b.P("// Give types to data labels") - for _, symbol := range profile.SymbolTable.Entries { - if symbol.DataType != "" { - quoted_data_type := strconv.Quote(symbol.DataType) - address := fmt.Sprintf("0x%08X", symbol.StartAddress) + for symbol := range profile.Symbols.Entries() { + if symbol.Symbol.DataType != "" { + quoted_data_type := strconv.Quote(symbol.Symbol.DataType) + address := fmt.Sprintf("0x%08X", symbol.Symbol.StartAddress) b.P("apply_type(%s, %s);", address, quoted_data_type) } } @@ -141,16 +159,16 @@ func (profile *Profile) generate_symbols_idc() (err error) { b.T(1) b.P("// Import function addresses and comments") b.P(`msg("Importing function addresses and comments");`) - for _, function_symbol := range profile.SymbolTable.Entries { - if function_symbol.Kind == symfile.Function { - address := fmt.Sprintf("0x%08X", function_symbol.StartAddress) + for function_symbol := range profile.Symbols.Entries() { + if function_symbol.Symbol.Kind == symbols.Function { + address := fmt.Sprintf("0x%08X", function_symbol.Symbol.StartAddress) // b.P("set_func_start(%s, %s);", address, address) // if function_symbol.EndAddress != 0 { // end_address := fmt.Sprintf("0x%08X", function_symbol.EndAddress) // b.P("set_func_end(%s, %s);", address, end_address) // } - if function_symbol.Comment != "" { - b.P("set_func_cmt(%s, %s, 0);", address, strconv.Quote(function_symbol.Comment)) + if function_symbol.Symbol.Comment != "" { + b.P("set_func_cmt(%s, %s, 0);", address, strconv.Quote(function_symbol.Symbol.Comment)) } } } @@ -159,10 +177,23 @@ func (profile *Profile) generate_symbols_idc() (err error) { b.Close() - return -} + // include stock scripts + ida_path := filepath.Join(profile.ArtifactsDirectory, "ida") + if err = os.MkdirAll(ida_path, 0755); err != nil { + return + } + if err = os.WriteFile(filepath.Join(ida_path, "import_all.idc"), import_all_script, 0755); err != nil { + return + } + if err = os.WriteFile(filepath.Join(ida_path, "import_data_types.idc"), import_data_types_script, 0755); err != nil { + return + } + if err = os.WriteFile(filepath.Join(ida_path, "import_functions.idc"), import_functions_script, 0755); err != nil { + return + } + if err = os.WriteFile(filepath.Join(ida_path, "import_symbols.idc"), import_symbols_script, 0755); err != nil { + return + } -func (profile *Profile) CreateIDAFiles() (err error) { - err = profile.generate_symbols_idc() return } diff --git a/go/profile/compile_x64dbg_artifacts.go b/go/profile/compile_x64dbg_artifacts.go new file mode 100644 index 0000000..ddb32a3 --- /dev/null +++ b/go/profile/compile_x64dbg_artifacts.go @@ -0,0 +1,9 @@ +package profile + +func compile_x64dbg_artifacts(profile *Profile, params *CompileArtifactsParams) (err error) { + if err = compile_x64dbg_types(profile); err != nil { + return + } + err = compile_x64dbg_database(profile, params.CompressX64dbgDatabase) + return +} diff --git a/go/profile/x64dbg_generate_database.go b/go/profile/compile_x64dbg_database.go similarity index 65% rename from go/profile/x64dbg_generate_database.go rename to go/profile/compile_x64dbg_database.go index f25847a..630223e 100644 --- a/go/profile/x64dbg_generate_database.go +++ b/go/profile/compile_x64dbg_database.go @@ -2,9 +2,10 @@ package profile import ( "fmt" + "os" "path/filepath" - "github.com/thunderbrewhq/binana/go/symfile" + "github.com/thunderbrewhq/binana/go/symbols" "github.com/thunderbrewhq/binana/go/x64dbg" ) @@ -12,7 +13,7 @@ func hex_address(u uint64) string { return fmt.Sprintf("0x%x", u) } -func (profile *Profile) generate_x64dbg_database(compress bool) (err error) { +func compile_x64dbg_database(profile *Profile, compress bool) (err error) { // Convert symbol table into x64dbg database is_64bit := profile.Info.Arch == "amd64" @@ -22,30 +23,30 @@ func (profile *Profile) generate_x64dbg_database(compress bool) (err error) { var dd x64dbg.Database - for _, entry := range profile.SymbolTable.Entries { - relative_start_address := entry.StartAddress - base_address + for entry := range profile.Symbols.Entries() { + relative_start_address := entry.Symbol.StartAddress - base_address relative_end_address := relative_start_address - if entry.EndAddress != 0 { - relative_end_address = entry.EndAddress - base_address + if entry.Symbol.EndAddress != 0 { + relative_end_address = entry.Symbol.EndAddress - base_address // for x64dbg, the end address is the last instruction. // for us, the end address is the address immediately after the last instruction. relative_end_address -= 1 } if relative_end_address < relative_start_address || relative_end_address-relative_start_address >= 50000 { - fmt.Printf("Strange symbol %s %08x %08x (offset %d)\n", entry.Name, relative_start_address, relative_end_address, relative_end_address-relative_start_address) + fmt.Printf("Strange symbol %s %08x %08x (offset %d)\n", entry.Symbol.Name, relative_start_address, relative_end_address, relative_end_address-relative_start_address) } // create label var label x64dbg.Label label.Manual = true label.Address = hex_address(relative_start_address) - label.Text = entry.Name + label.Text = entry.Symbol.Name label.Module = module_name dd.Labels = append(dd.Labels, label) - if entry.Kind == symfile.Function { + if entry.Symbol.Kind == symbols.Function { var fn x64dbg.Function fn.Manual = true fn.Start = hex_address(relative_start_address) @@ -66,8 +67,11 @@ func (profile *Profile) generate_x64dbg_database(compress bool) (err error) { } // save database - dd_path := filepath.Join(profile.Directory, "x64dbg", filename) - if err = x64dbg.SaveDatabase(dd_path, &dd, compress); err != nil { + dd_path := filepath.Join(profile.ArtifactsDirectory, "x64dbg") + if err = os.MkdirAll(dd_path, 0755); err != nil { + return + } + if err = x64dbg.SaveDatabase(filepath.Join(dd_path, filename), &dd, compress); err != nil { return } diff --git a/go/profile/x64dbg_generate_types.go b/go/profile/compile_x64dbg_types.go similarity index 95% rename from go/profile/x64dbg_generate_types.go rename to go/profile/compile_x64dbg_types.go index f21232c..0514435 100644 --- a/go/profile/x64dbg_generate_types.go +++ b/go/profile/compile_x64dbg_types.go @@ -2,6 +2,7 @@ package profile import ( "fmt" + "os" "path/filepath" "slices" "sort" @@ -98,7 +99,7 @@ loop: } // parses the C headers and generates a matching x64dbg types.json file -func (profile *Profile) generate_x64dbg_types() (err error) { +func compile_x64dbg_types(profile *Profile) (err error) { // parse C headers var cc_config cc.Config cc_config.ABI, err = cc.NewABI("windows", profile.Info.Arch) @@ -231,7 +232,11 @@ func (profile *Profile) generate_x64dbg_types() (err error) { x64_types.Structs = append(x64_types.Structs, x64_struct) } - types_file_path := filepath.Join(profile.Directory, "x64dbg", "types.json") + dd_path := filepath.Join(profile.ArtifactsDirectory, "x64dbg") + if err = os.MkdirAll(dd_path, 0755); err != nil { + return + } + types_file_path := filepath.Join(dd_path, "types.json") err = x64dbg.SortTypes(&x64_types) if err != nil { diff --git a/go/profile/generate.go b/go/profile/generate.go deleted file mode 100644 index 3409c17..0000000 --- a/go/profile/generate.go +++ /dev/null @@ -1,15 +0,0 @@ -package profile - -func (profile *Profile) Generate(compress_db bool) (err error) { - if err = profile.CreateIDAFiles(); err != nil { - return - } - - if profile.Info.OS == "windows" { - if err = profile.CreateX64dbgFiles(compress_db); err != nil { - return - } - } - - return -} diff --git a/go/profile/ida/import_all.idc b/go/profile/ida/import_all.idc new file mode 100644 index 0000000..008d2a3 --- /dev/null +++ b/go/profile/ida/import_all.idc @@ -0,0 +1,13 @@ +#include +#include "batch/import_symbols.idc" +#include "batch/import_data_types.idc" +#include "batch/import_functions.idc" + +static main() { + // Import all + import_symbols(); + import_data_types(); + import_functions(); + // Wait for auto-analysis to be finished + auto_wait(); +} diff --git a/go/profile/ida/import_data_types.idc b/go/profile/ida/import_data_types.idc new file mode 100644 index 0000000..8fa7059 --- /dev/null +++ b/go/profile/ida/import_data_types.idc @@ -0,0 +1,6 @@ +#include "batch/import_data_types.idc" + +static main() { + import_data_types(); + auto_wait(); +} \ No newline at end of file diff --git a/go/profile/ida/import_functions.idc b/go/profile/ida/import_functions.idc new file mode 100644 index 0000000..4527eda --- /dev/null +++ b/go/profile/ida/import_functions.idc @@ -0,0 +1,5 @@ +#include "batch/import_functions.idc" + +static main() { + import_functions(); +} \ No newline at end of file diff --git a/go/profile/ida/import_symbols.idc b/go/profile/ida/import_symbols.idc new file mode 100644 index 0000000..a549b43 --- /dev/null +++ b/go/profile/ida/import_symbols.idc @@ -0,0 +1,5 @@ +#include "batch/import_symbols.idc" + +static main() { + import_symbols(); +} \ No newline at end of file diff --git a/go/profile/open.go b/go/profile/open.go index 3bb95b6..4e87505 100644 --- a/go/profile/open.go +++ b/go/profile/open.go @@ -6,16 +6,18 @@ import ( "os" "path/filepath" - "github.com/thunderbrewhq/binana/go/symfile" + "github.com/thunderbrewhq/binana/go/symbols" ) type Profile struct { - Info Info - Directory string - SymbolTable *symfile.InMemoryTable + loaded bool + Info Info + Directory string + ArtifactsDirectory string + Symbols symbols.Table } -func Open(profile_directory string) (profile *Profile, err error) { +func (profile *Profile) Open(profile_directory, artifacts_directory string) (err error) { var dir fs.FileInfo dir, err = os.Stat(profile_directory) if err != nil { @@ -27,31 +29,23 @@ func Open(profile_directory string) (profile *Profile, err error) { return } - fmt.Println("Opening profile", profile_directory) + fmt.Println("opening profile", profile_directory) - profile = new(Profile) + profile.Symbols.Init() + profile.Directory = profile_directory + profile.ArtifactsDirectory = artifacts_directory + // read profile meta info if err = read_info(filepath.Join(profile_directory, "info.json"), &profile.Info); err != nil { return } - profile.Directory = profile_directory - - path_to_symbols_file := filepath.Join(profile_directory, "symbol", "main.sym") - var symbols_file *os.File - symbols_file, err = os.Open(path_to_symbols_file) - if err != nil { + // read symbols directory + if err = profile.Symbols.Load(filepath.Join(profile_directory, "symbol")); err != nil { return } - profile.SymbolTable = new(symfile.InMemoryTable) + profile.loaded = true - if err = symfile.Load(profile.SymbolTable, symbols_file); err != nil { - return - } - - symbols_file.Close() - - // return } diff --git a/go/profile/x64dbg.go b/go/profile/x64dbg.go deleted file mode 100644 index 77faac8..0000000 --- a/go/profile/x64dbg.go +++ /dev/null @@ -1,13 +0,0 @@ -package profile - -func (profile *Profile) CreateX64dbgFiles(compress_db bool) (err error) { - if err = profile.generate_x64dbg_database(compress_db); err != nil { - return - } - - if err = profile.generate_x64dbg_types(); err != nil { - return - } - - return -} diff --git a/go/symbols/error.go b/go/symbols/error.go new file mode 100644 index 0000000..db1ab86 --- /dev/null +++ b/go/symbols/error.go @@ -0,0 +1,8 @@ +package symbols + +import "fmt" + +var ( + ErrDuplicateSymbol = fmt.Errorf("symbols: a symbol already exists at that address") + ErrLineLeftEmpty = fmt.Errorf("symbols: line was left empty") +) diff --git a/go/symbols/loader.go b/go/symbols/loader.go new file mode 100644 index 0000000..817891d --- /dev/null +++ b/go/symbols/loader.go @@ -0,0 +1,79 @@ +package symbols + +import ( + "bufio" + "errors" + "fmt" + "io" + "strings" +) + +const min_columns = 3 + +type loader struct { + filename string + input *bufio.Reader + table *Table + line_number int +} + +func (l *loader) read_line() (line string, err error) { + line, err = l.input.ReadString('\n') + if err != nil { + return + } + line = strings.TrimRight(line, "\r\n") + l.line_number++ + return +} + +func (l *loader) parse_line(line string) (err error) { + var table_entry TableEntry + table_entry.Filename = l.filename + table_entry.Linenumber = l.line_number + + if line == "" { + //err = fmt.Errorf("%w: %s:%d", ErrLineLeftEmpty, table_entry.Filename, table_entry.Linenumber) + return + } + + if err = table_entry.Symbol.Parse(line); err != nil { + err = fmt.Errorf("%w: %s:%d", err, table_entry.Filename, table_entry.Linenumber) + return + } + + err = l.table.Insert(&table_entry) + return +} + +func load(filename string, file io.Reader, table *Table) (err error) { + l := new(loader) + + l.input = bufio.NewReader(file) + l.filename = filename + l.table = table + + var ( + line string + ) + + for { + line, err = l.read_line() + if err != nil { + if errors.Is(err, io.EOF) { + err = nil + break + } else { + err = fmt.Errorf("symbols: error reading at line %d: %w", l.line_number, err) + return + } + } + + if err = l.parse_line(line); err != nil { + err = fmt.Errorf("symbols: error parsing at line %d: %w", l.line_number, err) + return + } + } + + return +} diff --git a/go/symfile/parse_entry.go b/go/symbols/parse_symbol.go similarity index 85% rename from go/symfile/parse_entry.go rename to go/symbols/parse_symbol.go index d59180c..da73f7a 100644 --- a/go/symfile/parse_entry.go +++ b/go/symbols/parse_symbol.go @@ -1,4 +1,4 @@ -package symfile +package symbols import ( "fmt" @@ -69,7 +69,7 @@ func parse_attributes(attribute_columns []string) (attributes map[string]string, return } -func (entry *Entry) Parse(line string) (err error) { +func (symbol *Symbol) Parse(line string) (err error) { // trim extraneous whitespace line = strings.Trim(line, " \t") @@ -89,7 +89,7 @@ func (entry *Entry) Parse(line string) (err error) { // get name of symbol name_column := columns[0] if name_column == "" { - return fmt.Errorf("symfile: (*entry).Parse: entry has invalid name '%s", name_column) + return fmt.Errorf("symbols: (*Symbol).Parse: entry has invalid name '%s", name_column) } start_address, err = strconv.ParseUint(columns[1], 16, 64) @@ -99,10 +99,10 @@ func (entry *Entry) Parse(line string) (err error) { kind_column := columns[2] if len(kind_column) != 1 { - return fmt.Errorf("symfile: (*entry).Parse: entry has invalid kind") + return fmt.Errorf("symbols: (*Symbol).Parse: entry has invalid kind") } - kind := EntryKind(kind_column[0]) + kind := SymbolKind(kind_column[0]) if !slices.Contains(valid_kinds, kind) { return fmt.Errorf("symfile: (*entry).Parse: entry has invalid kind") @@ -122,10 +122,10 @@ func (entry *Entry) Parse(line string) (err error) { } // Start to build entry - entry.Name = name_column - entry.StartAddress = start_address - entry.Kind = kind - entry.Comment = comment_text + symbol.Name = name_column + symbol.StartAddress = start_address + symbol.Kind = kind + symbol.Comment = comment_text // build attributes if num_semantic_columns > 3 { @@ -138,11 +138,11 @@ func (entry *Entry) Parse(line string) (err error) { } if data_type, found := attributes["type"]; found { - entry.DataType = data_type + symbol.DataType = data_type } if end_address, found := attributes["end"]; found { - entry.EndAddress, err = strconv.ParseUint(end_address, 16, 64) + symbol.EndAddress, err = strconv.ParseUint(end_address, 16, 64) if err != nil { return } diff --git a/go/symbols/symbol.go b/go/symbols/symbol.go new file mode 100644 index 0000000..af5c8f4 --- /dev/null +++ b/go/symbols/symbol.go @@ -0,0 +1,77 @@ +package symbols + +import ( + "fmt" + "io" + "strings" +) + +// What kind of Symbol is this? +type SymbolKind uint8 + +const ( + // Something that can be executed + Function SymbolKind = 'f' + // Something that is read or written to + DataLabel SymbolKind = 'l' +) + +var ( + valid_kinds = []SymbolKind{Function, DataLabel} +) + +// An entry in the table +type Symbol struct { + // Undecorated, raw name + Name string + // Offset to the start of the function or data + StartAddress uint64 + // What kind of Symbol is this? + Kind SymbolKind + // Any table entry can have a comment after a ';' column + Comment string + // Attributes + // end=AABBCCEEDD + EndAddress uint64 + // type=void* + // The C syntax type of the data + DataType string +} + +func (entry *Symbol) String() string { + var b strings.Builder + entry.WriteTo(&b) + return b.String() +} + +func (entry *Symbol) WriteTo(w io.Writer) (n int64, err error) { + var b int + b, err = fmt.Fprintf(w, "%s %08X %c", entry.Name, entry.StartAddress, entry.Kind) + if err != nil { + return + } + n += int64(b) + if entry.EndAddress != 0 { + b, err = fmt.Fprintf(w, " end=%08X", entry.EndAddress) + if err != nil { + return + } + n += int64(b) + } + + if entry.DataType != "" { + b, err = fmt.Fprintf(w, " type=\"%s\"", entry.DataType) + if err != nil { + return + } + n += int64(b) + } + if entry.Comment != "" { + b, err = fmt.Fprintf(w, " ; %s", entry.Comment) + if err != nil { + return + } + n += int64(b) + } + return +} diff --git a/go/symbols/table.go b/go/symbols/table.go new file mode 100644 index 0000000..e10936a --- /dev/null +++ b/go/symbols/table.go @@ -0,0 +1,145 @@ +package symbols + +import ( + "fmt" + "io" + "iter" + "os" + "path/filepath" + "slices" + "strings" +) + +type TableEntry struct { + // the file where the symbol appears + Filename string + // the line number where the symbol appears in the file + Linenumber int + Symbol Symbol +} + +// Most tables are reasonably sized and can be kept in memory +type Table struct { + entries map[uint64]*TableEntry +} + +func (table *Table) Init() { + table.entries = make(map[uint64]*TableEntry) +} + +// Open opens a symbol file or a tree of symbol files. +func (table *Table) Load(name string) (err error) { + var ( + root_info os.FileInfo + ) + root_info, err = os.Stat(name) + if err != nil { + return + } + + if root_info.IsDir() { + var tree_entries []os.DirEntry + tree_entries, err = os.ReadDir(name) + if err != nil { + return + } + slices.SortFunc(tree_entries, func(a, b os.DirEntry) int { + return strings.Compare(a.Name(), b.Name()) + }) + for _, tree_entry := range tree_entries { + tree_child_name := filepath.Join(name, tree_entry.Name()) + if err = table.Load(tree_child_name); err != nil { + return + } + } + } else { + var symbol_file *os.File + symbol_file, err = os.Open(name) + if err != nil { + return + } + err = table.LoadFile(name, symbol_file) + symbol_file.Close() + if err != nil { + return + } + + } + + return +} + +func (table *Table) Entries() iter.Seq[*TableEntry] { + sorted_entries := make([]*TableEntry, len(table.entries)) + var i int + for _, entry := range table.entries { + sorted_entries[i] = entry + i++ + } + slices.SortFunc(sorted_entries, func(a, b *TableEntry) int { + if a.Symbol.StartAddress < b.Symbol.StartAddress { + return -1 + } else if a.Symbol.StartAddress == b.Symbol.StartAddress { + return 0 + } else { + return 1 + } + }) + return func(yield func(*TableEntry) bool) { + for _, entry := range sorted_entries { + if !yield(entry) { + break + } + } + } +} + +// LoadFile reads all symbols from the input stream +func (table *Table) LoadFile(name string, file io.Reader) (err error) { + err = load(name, file, table) + return +} + +func (table *Table) Insert(entry *TableEntry) (err error) { + conflicting_symbol, ok := table.entries[entry.Symbol.StartAddress] + if ok { + err = fmt.Errorf( + "%w: cannot insert symbol '%s' (%s:%d) because another symbol '%s' (%s:%d) already exists at that address '%08x'", + ErrDuplicateSymbol, + entry.Symbol.Name, + entry.Filename, + entry.Linenumber, + conflicting_symbol.Symbol.Name, + conflicting_symbol.Filename, + conflicting_symbol.Linenumber, + conflicting_symbol.Symbol.StartAddress, + ) + return + } + + table.entries[entry.Symbol.StartAddress] = entry + + return +} + +func (table *Table) WriteTo(w io.Writer) (n int64, err error) { + for entry := range table.Entries() { + var b int64 + b, err = entry.Symbol.WriteTo(w) + if err != nil { + return + } + n += b + _, err = w.Write([]byte("\n")) + if err != nil { + return + } + n++ + } + + return +} + +func (table *Table) Len() int { + return len(table.entries) +} diff --git a/go/symfile/loader.go b/go/symfile/loader.go deleted file mode 100644 index d9a6b77..0000000 --- a/go/symfile/loader.go +++ /dev/null @@ -1,70 +0,0 @@ -package symfile - -import ( - "bufio" - "errors" - "fmt" - "io" - "strings" -) - -const min_columns = 3 - -type loader struct { - input *bufio.Reader - table Table - line_number uint64 -} - -func (l *loader) read_line() (line string, err error) { - l.line_number++ - line, err = l.input.ReadString('\n') - if err != nil { - return - } - line = strings.TrimRight(line, "\r\n") - return -} - -func (l *loader) parse_line(line string) (err error) { - var entry Entry - if err = entry.Parse(line); err != nil { - err = fmt.Errorf("%w: line %d", err, l.line_number) - return - } - entry.LineNumber = l.line_number - - err = l.table.Insert(&entry) - return -} - -func load(text io.Reader, table Table) (err error) { - l := new(loader) - - l.input = bufio.NewReader(text) - l.table = table - - var ( - line string - ) - - for { - line, err = l.read_line() - if err != nil { - if errors.Is(err, io.EOF) { - err = nil - break - } else { - err = fmt.Errorf("symfile: error reading at line %d: %w", l.line_number, err) - return - } - } - - if err = l.parse_line(line); err != nil { - err = fmt.Errorf("symfile: error parsing at line %d: %w", l.line_number, err) - return - } - } - - return -} diff --git a/go/symfile/memory_table.go b/go/symfile/memory_table.go deleted file mode 100644 index e56856d..0000000 --- a/go/symfile/memory_table.go +++ /dev/null @@ -1,38 +0,0 @@ -package symfile - -import ( - "fmt" - "slices" - "sort" -) - -// Most tables are reasonably-sized and can be kept in memory -type InMemoryTable struct { - Entries []Entry -} - -func (t *InMemoryTable) Insert(entry *Entry) (err error) { - i := sort.Search(len(t.Entries), func(i int) bool { - return t.Entries[i].StartAddress >= entry.StartAddress - }) - - if i < len(t.Entries) { - if t.Entries[i].StartAddress == entry.StartAddress { - err = fmt.Errorf("symfile: (*InMemoryTable).Insert() failed: duplicate entry: %s", entry.Name) - return - } - t.Entries = slices.Insert(t.Entries, i, *entry) - } else { - t.Entries = append(t.Entries, *entry) - } - - return -} - -func (t *InMemoryTable) Len() int { - return len(t.Entries) -} - -func NewInMemoryTable() *InMemoryTable { - return new(InMemoryTable) -} diff --git a/go/symfile/symfile.go b/go/symfile/symfile.go deleted file mode 100644 index 3aaed4e..0000000 --- a/go/symfile/symfile.go +++ /dev/null @@ -1,64 +0,0 @@ -package symfile - -import ( - "fmt" - "io" - "strings" -) - -// What kind of Entry is this? -type EntryKind uint8 - -const ( - // Something that can be executed - Function EntryKind = 'f' - // Something that is read or written to - DataLabel EntryKind = 'l' -) - -var ( - valid_kinds = []EntryKind{Function, DataLabel} -) - -// An entry in the table -type Entry struct { - LineNumber uint64 - // Undecorated, raw name - Name string - // Offset to the start of the function or data - StartAddress uint64 - // What kind of Entry is this? - Kind EntryKind - // Any table entry can have a comment after a ';' column - Comment string - // Attributes - // end=AABBCCEEDD - EndAddress uint64 - // type=void* - // The C syntax type of the data - DataType string -} - -func (entry *Entry) String() string { - var b strings.Builder - fmt.Fprintf(&b, "%s %08X %c", entry.Name, entry.StartAddress, entry.Kind) - if entry.EndAddress != 0 { - fmt.Fprintf(&b, " end=%08X", entry.EndAddress) - } - if entry.DataType != "" { - fmt.Fprintf(&b, " type=\"%s\"", entry.DataType) - } - if entry.Comment != "" { - fmt.Fprintf(&b, " ; %s", entry.Comment) - } - return b.String() -} - -type Table interface { - Insert(entry *Entry) (err error) -} - -func Load(table Table, text io.Reader) (err error) { - err = load(text, table) - return -}