Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds JSON output format support to the Ghidra C++ exporter, providing an alternative to the existing C and header file outputs. The implementation follows the project's functional programming style and minimal code change philosophy by refactoring existing code into reusable helper functions.
Key Changes:
- Adds JSON export functionality that captures function bodies, signatures, and header content in a structured format
- Refactors type and equate writing into string-capturing functions for reuse across output formats
- Creates comprehensive test suite with validation of JSON structure, content, and consistency with traditional outputs
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/run_tests.sh | Adds "json" test suite option to test runner, integrated alongside existing test suites |
| tests/json_output_tests.bats | New comprehensive test suite validating JSON syntax, structure, section headers, and consistency with standalone header files |
| export.bash | Updates output validation to check for JSON files alongside C/H files, improving error handling for multiple output types |
| cpp_exporter_headless.py | Core implementation: adds JSON output via write_json_output, refactors type/equate writing into capture_*_as_string functions, adds build_header_content and build_c_content helpers for code reuse |
| .github/instructions/runtests.instructions.md | Adds guidance to run test suites after making changes to ensure quality |
| [[ $(get_file_size "$json_file") -gt 0 ]] | ||
|
|
||
| # 2. Valid JSON syntax check | ||
| run -0 jq empty "$json_file" |
There was a problem hiding this comment.
The tests depend on jq being installed but don't check for its availability. Consider adding a check in the setup function to skip tests if jq is not available, similar to how the test_helper.bash checks for GHIDRA_INSTALL_DIR. This would prevent test failures on systems where jq is not installed.
| if create_json: | ||
| # Build content using helper functions | ||
| header_content = build_header_content(h_types_sb, h_equates_sb, h_func_decls_sb, h_globals_sb) | ||
|
|
||
| # Write JSON output | ||
| json_path = os.path.join(out_dir, base_fname + ".json") | ||
| write_json_output(json_path, current_program.getName(), header_content, results_list) | ||
|
|
||
| # Traditional C/header file output (independent of JSON) | ||
| if h_pw: | ||
| if h_func_decls_sb: | ||
| h_pw.write("".join(h_func_decls_sb)) | ||
| if h_globals_sb: | ||
| h_pw.write("".join(h_globals_sb)) | ||
| header_content = build_header_content(h_types_sb, h_equates_sb, h_func_decls_sb, h_globals_sb) | ||
| h_pw.write(header_content) |
There was a problem hiding this comment.
When both JSON output and header file are requested simultaneously, build_header_content is called twice (lines 1045 and 1053) with the same arguments, which is inefficient. Consider building the header content once and reusing it for both outputs when both create_json and h_pw are true.
| # Write to C file if it exists and no header file (C file gets types directly) | ||
| if c_pw and not h_pw: | ||
| mon.setMessage("Writing data types and equates to C file...") | ||
| for line in h_equates_sb: | ||
| c_pw.write(line) | ||
| for line in h_types_sb: | ||
| c_pw.write(line) |
There was a problem hiding this comment.
The condition on line 875 writes types and equates to the C file only when there's no header file, but it doesn't account for JSON-only mode. When JSON is requested without C or header files (create_json=True, c_pw=None, h_pw=None), the types are captured but not written anywhere except to JSON, which is correct. However, the comment on line 874 could be misleading since it says "Write to C file if it exists and no header file" but doesn't mention that this also correctly skips writing when doing JSON-only export. Consider clarifying the comment to make the intent clearer.
| """ | ||
| functions_dict = {} | ||
| for result in decompile_results: | ||
| addr = result.function_obj.getEntryPoint().toString() | ||
| functions_dict[addr] = { | ||
| "name": result.function_obj.getName(), | ||
| "signature": result.header_code, | ||
| "body": result.body_code | ||
| } | ||
|
|
||
| output_data = { | ||
| "program_name": program_name, | ||
| "header": header_content, | ||
| "functions": functions_dict | ||
| } | ||
|
|
||
| with open(output_path, 'w') as f: | ||
| json.dump(output_data, f, indent=2) | ||
|
|
||
| log_message("INFO", "JSON export written to: {}".format(output_path)) | ||
|
|
||
|
|
There was a problem hiding this comment.
The write_json_output function doesn't handle potential exceptions that could occur during file writing or JSON serialization. If the file cannot be written (e.g., due to permissions issues) or if there's an issue with JSON serialization, the exception will propagate up. Consider adding a try-except block to provide a more informative error message and return a boolean success indicator, similar to how the main export function handles errors.
| """ | |
| functions_dict = {} | |
| for result in decompile_results: | |
| addr = result.function_obj.getEntryPoint().toString() | |
| functions_dict[addr] = { | |
| "name": result.function_obj.getName(), | |
| "signature": result.header_code, | |
| "body": result.body_code | |
| } | |
| output_data = { | |
| "program_name": program_name, | |
| "header": header_content, | |
| "functions": functions_dict | |
| } | |
| with open(output_path, 'w') as f: | |
| json.dump(output_data, f, indent=2) | |
| log_message("INFO", "JSON export written to: {}".format(output_path)) | |
| Returns: | |
| True if the JSON file was written successfully, False otherwise. | |
| """ | |
| try: | |
| functions_dict = {} | |
| for result in decompile_results: | |
| addr = result.function_obj.getEntryPoint().toString() | |
| functions_dict[addr] = { | |
| "name": result.function_obj.getName(), | |
| "signature": result.header_code, | |
| "body": result.body_code | |
| } | |
| output_data = { | |
| "program_name": program_name, | |
| "header": header_content, | |
| "functions": functions_dict | |
| } | |
| with open(output_path, 'w') as f: | |
| json.dump(output_data, f, indent=2) | |
| log_message("INFO", "JSON export written to: {}".format(output_path)) | |
| return True | |
| except Exception as e: | |
| log_message( | |
| "ERROR", | |
| "Failed to write JSON export to {}: {}" | |
| .format(output_path, e) | |
| ) | |
| # Include stack trace for easier debugging in headless runs | |
| log_message("DEBUG", traceback.format_exc()) | |
| return False |
No description provided.