Crusader_Decomp/tools/pyghidra_crusader/cli.py
MaddoScientisto 24d4416003 Add various scripts and JSON plans for Ghidra project
- Introduced `seg043_boundary_repair.json` to manage function boundaries in segment 043.
- Created `read_file.py` for reading and printing file content size.
- Added `resolve_bb4f.py` to resolve specific function call targets.
- Implemented `resolve_top_targets.py` to find resolved NE targets for top-called wrapper functions.
- Added `script_contents.txt` to summarize NE relocation far calls.
- Updated `tier4_ghidra.txt`, `tier4_ghidra_check.txt`, `tier4_output.txt`, and `tier4_result.txt` with function call statistics.
- Created `tier5_errors.txt` for error logging and `tier5_output.txt` for additional function call statistics.
- Established `tools` directory with helper scripts for the Ghidra project, including CLI and common functionalities.
- Implemented command-line interface in `cli.py` for various project operations.
- Added `common.py` for shared functions and configurations across tools.
- Introduced `validate_fixups.py` to validate NE relocation fixups against known addresses.
2026-03-20 23:50:39 +01:00

258 lines
No EOL
8.6 KiB
Python

from __future__ import annotations
import argparse
import json
from pathlib import Path
from .common import (
DEFAULT_INSTALL_DIR,
DEFAULT_PROJECT_DIR,
DEFAULT_PROJECT_NAME,
DEFAULT_PROGRAM_NAME,
DEFAULT_FOLDER_PATH,
ProjectConfig,
create_function,
get_function,
list_root_files,
open_program,
open_project,
remove_function,
rename_function,
save_program,
set_comment,
transaction,
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="PyGhidra helpers for the Crusader project."
)
parser.add_argument(
"--install-dir",
default=str(DEFAULT_INSTALL_DIR),
help="Ghidra install directory.",
)
parser.add_argument(
"--project-dir",
default=str(DEFAULT_PROJECT_DIR),
help="Directory containing the Ghidra project.",
)
parser.add_argument(
"--project-name",
default=DEFAULT_PROJECT_NAME,
help="Ghidra project name.",
)
parser.add_argument(
"--program-name",
default=DEFAULT_PROGRAM_NAME,
help="Program name inside the project.",
)
parser.add_argument(
"--folder-path",
default=DEFAULT_FOLDER_PATH,
help="Project folder path containing the program.",
)
parser.add_argument(
"--restore-project",
action="store_true",
help="Restore project tool state while opening the project.",
)
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser(
"project-files",
help="List root-level files in the Ghidra project.",
)
create_parser = subparsers.add_parser(
"create-function",
help="Create a function at an address with an optional explicit body range.",
)
create_parser.add_argument("--entry", required=True, help="Function entry address.")
create_parser.add_argument("--name", required=True, help="New function name.")
create_parser.add_argument("--body-start", help="Function body start address.")
create_parser.add_argument("--body-end", help="Function body end address.")
create_parser.add_argument(
"--plate-comment",
help="Optional plate comment to set at the entry address after creation.",
)
delete_parser = subparsers.add_parser(
"delete-function",
help="Delete a function at an address.",
)
delete_parser.add_argument("--entry", required=True, help="Function entry address.")
rename_parser = subparsers.add_parser(
"rename-function",
help="Rename an existing function by entry address.",
)
rename_parser.add_argument("--entry", required=True, help="Function entry address.")
rename_parser.add_argument("--name", required=True, help="New function name.")
comment_parser = subparsers.add_parser(
"set-comment",
help="Set a code-unit comment by address.",
)
comment_parser.add_argument("--address", required=True, help="Comment target address.")
comment_parser.add_argument("--text", required=True, help="Comment text.")
comment_parser.add_argument(
"--type",
choices=["pre", "plate", "eol", "repeatable", "post"],
default="plate",
help="Comment type.",
)
plan_parser = subparsers.add_parser(
"apply-plan",
help="Apply a JSON edit plan containing function and comment operations.",
)
plan_parser.add_argument("--plan", required=True, help="Path to the JSON plan file.")
plan_parser.add_argument(
"--dry-run",
action="store_true",
help="Validate and print the plan without modifying the project.",
)
return parser
def build_config(args: argparse.Namespace) -> ProjectConfig:
return ProjectConfig(
install_dir=Path(args.install_dir),
project_dir=Path(args.project_dir),
project_name=args.project_name,
program_name=args.program_name,
folder_path=args.folder_path,
restore_project=args.restore_project,
)
def command_project_files(config: ProjectConfig, _args: argparse.Namespace) -> int:
project = open_project(config)
try:
for name in list_root_files(project):
print(name)
finally:
project.close()
return 0
def command_create_function(config: ProjectConfig, args: argparse.Namespace) -> int:
with open_program(config, read_only=False) as (project, program):
with transaction(program, f"Create function {args.entry}"):
function = create_function(program, args.entry, args.name, args.body_start, args.body_end)
if args.plate_comment:
set_comment(program, args.entry, args.plate_comment, "plate")
save_program(project, program)
print(f"created {function.getName()} at {args.entry}")
return 0
def command_delete_function(config: ProjectConfig, args: argparse.Namespace) -> int:
with open_program(config, read_only=False) as (project, program):
with transaction(program, f"Delete function {args.entry}"):
removed = remove_function(program, args.entry)
if not removed:
raise RuntimeError(f"no function removed at {args.entry}")
save_program(project, program)
print(f"deleted function at {args.entry}")
return 0
def command_rename_function(config: ProjectConfig, args: argparse.Namespace) -> int:
with open_program(config, read_only=False) as (project, program):
with transaction(program, f"Rename function {args.entry}"):
function = rename_function(program, args.entry, args.name)
save_program(project, program)
print(f"renamed {args.entry} to {function.getName()}")
return 0
def command_set_comment(config: ProjectConfig, args: argparse.Namespace) -> int:
with open_program(config, read_only=False) as (project, program):
with transaction(program, f"Set comment {args.address}"):
set_comment(program, args.address, args.text, args.type)
save_program(project, program)
print(f"set {args.type} comment at {args.address}")
return 0
def _load_plan(plan_path: str) -> dict:
with open(plan_path, "r", encoding="utf-8") as handle:
return json.load(handle)
def _print_plan(plan: dict) -> None:
print(json.dumps(plan, indent=2, sort_keys=True))
def command_apply_plan(config: ProjectConfig, args: argparse.Namespace) -> int:
plan = _load_plan(args.plan)
if args.dry_run:
_print_plan(plan)
return 0
transaction_name = plan.get("transaction", f"Apply plan {args.plan}")
with open_program(config, read_only=False) as (project, program):
with transaction(program, transaction_name):
for entry in plan.get("remove_functions", []):
removed = remove_function(program, entry)
if not removed:
raise RuntimeError(f"no function removed at {entry}")
for entry in plan.get("rename_functions", []):
rename_function(program, entry["entry"], entry["name"])
for entry in plan.get("create_functions", []):
create_function(
program,
entry["entry"],
entry["name"],
entry.get("body_start"),
entry.get("body_end"),
)
if entry.get("comment"):
set_comment(
program,
entry["entry"],
entry["comment"],
entry.get("comment_type", "plate"),
)
for entry in plan.get("comments", []):
set_comment(
program,
entry["address"],
entry["text"],
entry.get("type", "plate"),
)
for entry in plan.get("assert_functions", []):
if get_function(program, entry) is None:
raise RuntimeError(f"expected function missing at {entry}")
save_program(project, program)
print(f"applied plan {args.plan}")
return 0
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
config = build_config(args)
command_map = {
"project-files": command_project_files,
"create-function": command_create_function,
"delete-function": command_delete_function,
"rename-function": command_rename_function,
"set-comment": command_set_comment,
"apply-plan": command_apply_plan,
}
return command_map[args.command](config, args)
if __name__ == "__main__":
raise SystemExit(main())