Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- Program edits are now persisted to the project database after every mutating
command. Previously, commands such as `comment set`, `symbol rename`, `patch
bytes`, `type` edits, `tag` edits and script/batch runs only ended their Ghidra
transaction, which commits into the bridge JVM's in-memory program; nothing
flushed to the `.rep` store until an `analyze`/program switch/`import`. Since
`bridge stop` kills the JVM without a teardown save, those edits were lost on
teardown and never showed up when the project was opened in the Ghidra GUI.
Read-only queries are unaffected, and a failed save is logged without failing
the command.

## [0.2.2]

### Added
Expand Down
94 changes: 93 additions & 1 deletion src/ghidra/scripts/GhidraCliBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -668,12 +668,74 @@ private HandleResult executeProgramRequest(String command, JsonObject args) {
return new HandleResult(errorResponse(result.get("error").getAsString()), false);
}

// Persist mutations to the project database so they are visible
// when the program is later opened in the Ghidra GUI. Ending a
// transaction (endTransaction) only commits into the bridge JVM's
// in-memory program; only program.save() flushes to the .rep store.
// import/analyze/open_program already save on their own paths and
// are excluded here to avoid redundant writes.
if (isMutatingCommand(command) && currentProgram != null) {
try {
currentProgram.save("ghidra-cli: " + command, monitor);
} catch (Exception saveErr) {
printerr("Auto-save after " + command + " failed: " + saveErr.getMessage());
}
}

return new HandleResult(successResponse(result), false);
} catch (Exception e) {
return new HandleResult(errorResponse(e.getMessage()), false);
}
}

/**
* Commands that mutate the program and must be flushed to the project's
* .rep database with program.save() so the changes survive bridge teardown
* and appear in the Ghidra GUI. Read-only queries (decompile, disasm,
* list_*, xrefs_*, find_*, get_*, graph_*, diff_*, exports) are omitted, as
* are import/analyze/open_program which persist on their own code paths.
*/
private boolean isMutatingCommand(String command) {
if (command == null) return false;
switch (command) {
case "create_function":
case "rename_function":
case "delete_function":
case "symbol_create":
case "symbol_delete":
case "symbol_rename":
case "type_create":
case "type_apply":
case "type_delete":
case "type_rename":
case "type_create_enum":
case "type_typedef":
case "type_add_field":
case "type_del_field":
case "function_set_signature":
case "function_set_return_type":
case "function_set_calling_convention":
case "set_var_type":
case "comment_set":
case "comment_delete":
case "patch_bytes":
case "patch_nop":
case "script_run":
case "script_java":
case "script_python":
case "tag_create":
case "tag_delete":
case "tag_rename":
case "tag_set_comment":
case "tag_add":
case "tag_remove":
case "batch":
return true;
default:
return false;
}
}

private JsonObject dispatchCommand(String command, JsonObject args) {
if (command == null) return null;
switch (command) {
Expand Down Expand Up @@ -1410,7 +1472,37 @@ private JsonObject handleCreateFunction(JsonObject args) {

int txId = currentProgram.startTransaction("Create function");
try {
Function created = fm.createFunction(functionName, addr, null, SourceType.USER_DEFINED);
Function created = null;
try {
created = fm.createFunction(functionName, addr, null, SourceType.USER_DEFINED);
} catch (Exception strictErr) {
// Strict FunctionManager API rejects raw/flat-firmware gap entries
// with "Function body must contain the entrypoint". Fall back to
// the GUI-equivalent command below.
created = null;
}

if (created == null) {
// CreateFunctionCmd disassembles at the entry if needed and
// computes the body by following flow, matching what the Ghidra
// GUI's "Create Function" does. This is what lets computed/indirect
// gap targets (common on flat firmware images) become real functions.
ghidra.app.cmd.function.CreateFunctionCmd cmd =
new ghidra.app.cmd.function.CreateFunctionCmd(addr);
boolean applied = cmd.applyTo(currentProgram, monitor);
if (applied) {
created = fm.getFunctionAt(addr);
if (created != null && requestedName != null && !requestedName.isEmpty()) {
try {
created.setName(functionName, SourceType.USER_DEFINED);
} catch (Exception nameErr) {
printerr("Function created but naming failed at " + addr
+ ": " + nameErr.getMessage());
}
}
}
}

if (created == null) {
currentProgram.endTransaction(txId, false);
return errorResult("Failed to create function at " + addr.toString());
Expand Down