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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -26,16 +35,22 @@ public class RevEngAIAnalysisOptionsDialog extends RevEngDialogComponentProvider
private JComboBox<String> 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() {
Expand All @@ -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",
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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(
"<html><center>File size (%s) exceeds<br>server limit (%s)</center></html>"
.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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,6 +49,7 @@
public class TypedApiImplementation implements TypedApiInterface {
private final HttpClient httpClient;
private final String baseUrl;
private final ConfigApi configApi;
Map<String, String> headers;

private final AnalysesCoreApi analysisCoreApi;
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -666,5 +669,13 @@ public List<String> getAssembly(FunctionID id) {
return result;
}

@Override
public ConfigResponse getConfig() {
try {
return this.configApi.getConfig().getData();
} catch (ApiException e) {
throw new RuntimeException(e);
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -225,5 +226,8 @@ default List<String> getAssembly(FunctionID functionID) throws ApiException {
throw new UnsupportedOperationException("getAssembly not implemented yet");
}

default ConfigResponse getConfig() throws ApiException {
throw new UnsupportedOperationException("getConfig not implemented yet");
}
}

This file was deleted.

11 changes: 7 additions & 4 deletions src/test/java/ai/reveng/AnalysisOptionsDialogTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@

import static org.junit.Assert.*;

import java.util.*;

import javax.swing.*;

import ai.reveng.toolkit.ghidra.binarysimilarity.ui.analysiscreation.RevEngAIAnalysisOptionsDialog;
Expand All @@ -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();
Expand Down
Loading