diff --git a/build.gradle b/build.gradle index 3903754..f7bfad9 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ dependencies { implementation 'org.json:json:20250107' implementation "com.google.guava:guava:33.2.0-jre" implementation group: 'com.fifesoft', name: 'rsyntaxtextarea', version: '3.5.2' - implementation "ai.reveng:sdk:2.52.1" + implementation('ai.reveng:sdk:3.0.0') testImplementation('junit:junit:4.13.1') testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2") diff --git a/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/analysiscreation/RevEngAIAnalysisOptionsDialog.java b/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/analysiscreation/RevEngAIAnalysisOptionsDialog.java index 92c00e6..30df141 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/analysiscreation/RevEngAIAnalysisOptionsDialog.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/analysiscreation/RevEngAIAnalysisOptionsDialog.java @@ -1,21 +1,30 @@ package ai.reveng.toolkit.ghidra.binarysimilarity.ui.analysiscreation; +import ai.reveng.model.ConfigResponse; import ai.reveng.toolkit.ghidra.binarysimilarity.ui.dialog.RevEngDialogComponentProvider; import ai.reveng.toolkit.ghidra.core.services.api.AnalysisOptionsBuilder; import ai.reveng.toolkit.ghidra.core.services.api.GhidraRevengService; import ai.reveng.toolkit.ghidra.core.services.api.types.AnalysisScope; import ai.reveng.toolkit.ghidra.plugins.ReaiPluginPackage; import ghidra.program.model.listing.Program; +import ghidra.util.Msg; +import ghidra.util.Swing; import javax.annotation.Nullable; import javax.swing.*; import java.awt.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; import java.util.List; +import java.util.concurrent.CompletableFuture; public class RevEngAIAnalysisOptionsDialog extends RevEngDialogComponentProvider { private JCheckBox advancedAnalysisCheckBox; private JCheckBox dynamicExecutionCheckBox; private final Program program; + private final GhidraRevengService service; private JRadioButton privateScope; private JRadioButton publicScope; private JTextField tagsTextBox; @@ -26,16 +35,22 @@ public class RevEngAIAnalysisOptionsDialog extends RevEngDialogComponentProvider private JComboBox architectureComboBox; private boolean okPressed = false; + private JLabel fileSizeWarningLabel; + private JLabel loadingLabel; + public static RevEngAIAnalysisOptionsDialog withModelsFromServer(Program program, GhidraRevengService reService) { - return new RevEngAIAnalysisOptionsDialog(program); + return new RevEngAIAnalysisOptionsDialog(program, reService); } - public RevEngAIAnalysisOptionsDialog(Program program) { + public RevEngAIAnalysisOptionsDialog(Program program, GhidraRevengService service) { super(ReaiPluginPackage.WINDOW_PREFIX + "Configure Analysis for %s".formatted(program.getName()), true); this.program = program; + this.service = service; buildInterface(); - setPreferredSize(320, 380); + setPreferredSize(320, 420); + + fetchConfigAsync(); } private void buildInterface() { @@ -48,6 +63,15 @@ private void buildInterface() { JPanel titlePanel = createTitlePanel("Create new analysis for this binary"); workPanel.add(titlePanel, BorderLayout.NORTH); + // File size warning label (hidden by default) + fileSizeWarningLabel = new JLabel(); + fileSizeWarningLabel.setForeground(Color.RED); + fileSizeWarningLabel.setHorizontalAlignment(SwingConstants.CENTER); + fileSizeWarningLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + fileSizeWarningLabel.setVisible(false); + fileSizeWarningLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + workPanel.add(fileSizeWarningLabel); + // Add Platform Drop Down var platformComboBox = new JComboBox<>(new String[]{ "Auto", "windows", "linux", @@ -144,10 +168,19 @@ private void buildInterface() { workPanel.add(tagsLabel); workPanel.add(tagsTextBox); + // Loading indicator (shown while fetching config) + loadingLabel = new JLabel("Checking file size limits..."); + loadingLabel.setForeground(Color.GRAY); + loadingLabel.setHorizontalAlignment(SwingConstants.CENTER); + loadingLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + loadingLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + workPanel.add(loadingLabel); + addCancelButton(); addOKButton(); okButton.setText("Start Analysis"); + okButton.setEnabled(false); // Disabled until config check completes } public @Nullable AnalysisOptionsBuilder getOptionsFromUI() { @@ -187,4 +220,80 @@ protected void okCallback() { public JComponent getComponent() { return super.getComponent(); } + + private void fetchConfigAsync() { + CompletableFuture.supplyAsync(() -> { + try { + return service.getApi().getConfig(); + } catch (Exception e) { + Msg.warn(this, "Failed to fetch server config: " + e.getMessage()); + return null; + } + }).thenAccept(config -> { + Swing.runNow(() -> handleConfigResponse(config)); + }); + } + + private void handleConfigResponse(@Nullable ConfigResponse config) { + loadingLabel.setVisible(false); + + if (config == null) { + // Config fetch failed, allow upload attempt (server will reject if too large) + okButton.setEnabled(true); + return; + } + + long maxFileSizeBytes = config.getMaxFileSizeBytes().longValue(); + validateFileSize(maxFileSizeBytes); + } + + private void validateFileSize(long maxFileSizeBytes) { + long fileSize = getProgramFileSize(); + if (fileSize < 0) { + // Could not determine file size, allow upload attempt + okButton.setEnabled(true); + return; + } + + if (fileSize > maxFileSizeBytes) { + String fileSizeStr = formatBytes(fileSize); + String maxSizeStr = formatBytes(maxFileSizeBytes); + fileSizeWarningLabel.setText( + "
File size (%s) exceeds
server limit (%s)
" + .formatted(fileSizeStr, maxSizeStr)); + fileSizeWarningLabel.setVisible(true); + okButton.setEnabled(false); + } else { + fileSizeWarningLabel.setVisible(false); + okButton.setEnabled(true); + } + } + + private long getProgramFileSize() { + try { + Path filePath; + try { + filePath = Path.of(program.getExecutablePath()); + } catch (InvalidPathException e) { + // Windows paths may have leading slash like "/C:/file.dll" + filePath = Path.of(program.getExecutablePath().substring(1)); + } + return Files.size(filePath); + } catch (IOException | InvalidPathException e) { + Msg.warn(this, "Could not determine file size: " + e.getMessage()); + return -1; + } + } + + private static String formatBytes(long bytes) { + if (bytes < 1024) { + return bytes + " B"; + } else if (bytes < 1024 * 1024) { + return "%.1f KB".formatted(bytes / 1024.0); + } else if (bytes < 1024 * 1024 * 1024) { + return "%.1f MB".formatted(bytes / (1024.0 * 1024)); + } else { + return "%.1f GB".formatted(bytes / (1024.0 * 1024 * 1024)); + } + } } diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java index d4485ce..796eff7 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java @@ -2,6 +2,7 @@ import ai.reveng.api.*; import ai.reveng.model.*; +import ai.reveng.model.ConfigResponse; import ai.reveng.toolkit.ghidra.core.services.api.types.*; import ai.reveng.toolkit.ghidra.core.services.api.types.Collection; import ai.reveng.toolkit.ghidra.core.services.api.types.FunctionMatch; @@ -48,6 +49,7 @@ public class TypedApiImplementation implements TypedApiInterface { private final HttpClient httpClient; private final String baseUrl; + private final ConfigApi configApi; Map headers; private final AnalysesCoreApi analysisCoreApi; @@ -101,6 +103,7 @@ public TypedApiImplementation(String baseUrl, String apiKey) { this.functionsRenamingHistoryApi = new FunctionsRenamingHistoryApi(apiClient); this.functionsAiDecompilationApi = new FunctionsAiDecompilationApi(apiClient); this.functionsDataTypesApi = new FunctionsDataTypesApi(apiClient); + this.configApi = new ConfigApi(apiClient); this.baseUrl = baseUrl + "/"; this.httpClient = HttpClient.newBuilder() @@ -666,5 +669,13 @@ public List getAssembly(FunctionID id) { return result; } + @Override + public ConfigResponse getConfig() { + try { + return this.configApi.getConfig().getData(); + } catch (ApiException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java index 99ea072..3fe186b 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java @@ -28,6 +28,7 @@ * */ public interface TypedApiInterface { + /// Data type to represent the RevEng.AI API concept of a function ID record FunctionID(long value){ public Integer asInteger() { @@ -225,5 +226,8 @@ default List getAssembly(FunctionID functionID) throws ApiException { throw new UnsupportedOperationException("getAssembly not implemented yet"); } + default ConfigResponse getConfig() throws ApiException { + throw new UnsupportedOperationException("getConfig not implemented yet"); + } } diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/ConfigResponse.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/ConfigResponse.java deleted file mode 100644 index 31f2d36..0000000 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/ConfigResponse.java +++ /dev/null @@ -1,73 +0,0 @@ -package ai.reveng.toolkit.ghidra.core.services.api.types; - -import org.json.JSONObject; - -import java.util.List; - -/** - * { - * "success": true, - * "max_file_size": 6291456, - * "valid_models": [ - * "binnet-0.3-x86" - * ], - * "isa_options": [ - * "Auto", - * "x86", - * "x86_64" - * ], - * "file_options": [ - * "Auto", - * "PE", - * "ELF", - * "RAW", - * "EXE", - * "dll", - * "Mach-O" - * ], - * "platform_options": [ - * "Auto", - * "windows", - * "linux", - * "android", - * "macos" - * ], - * "analysis_status_conditions": [ - * "Complete", - * "Error", - * "Processing", - * "Queued", - * "All" - * ], - * "analysis_scope_conditions": [ - * "PUBLIC", - * "PRIVATE", - * "ALL" - * ] - * } - */ -public record ConfigResponse( - boolean success, - int max_file_size, - List valid_models, - List isa_options, - List file_options, - List platform_options, - List analysis_status_conditions, - List analysis_scope_conditions -) { - public static ConfigResponse fromJSONObject(JSONObject json) { - throw new UnsupportedOperationException("Not implemented yet."); -// return new ConfigResponse( -// json.getBoolean("success"), -// json.getInt("max_file_size"), -// json.getJSONArray("valid_models").toList(), -// json.getJSONArray("isa_options").toList(), -// json.getJSONArray("file_options").toList(), -// json.getJSONArray("platform_options").toList(), -// json.getJSONArray("analysis_status_conditions").toList(), -// json.getJSONArray("analysis_scope_conditions").toList() -// ); - } - -} diff --git a/src/test/java/ai/reveng/AnalysisOptionsDialogTest.java b/src/test/java/ai/reveng/AnalysisOptionsDialogTest.java index f5e2d6b..6d9e0cb 100644 --- a/src/test/java/ai/reveng/AnalysisOptionsDialogTest.java +++ b/src/test/java/ai/reveng/AnalysisOptionsDialogTest.java @@ -17,8 +17,6 @@ import static org.junit.Assert.*; -import java.util.*; - import javax.swing.*; import ai.reveng.toolkit.ghidra.binarysimilarity.ui.analysiscreation.RevEngAIAnalysisOptionsDialog; @@ -41,17 +39,22 @@ public AnalysisOptionsDialogTest() { } @Test - public void testWithMockModels() throws Exception { + public void testBasicOptionsDialog() throws Exception { var reService = new GhidraRevengService( new MockApi() {}); var builder = new ProgramBuilder("mock", ProgramBuilder._X64, this); - var program = builder.getProgram(); var dialog = RevEngAIAnalysisOptionsDialog.withModelsFromServer(program, reService); SwingUtilities.invokeLater(() -> { DockingWindowManager.showDialog(null, dialog); }); waitForSwing(); + waitFor( + () -> { + JButton okButton = (JButton) getInstanceField("okButton", dialog); + return okButton.isEnabled(); + } + ); runSwing(() -> { JButton okButton = (JButton) getInstanceField("okButton", dialog); okButton.doClick();