Skip to content

Commit 95aa52a

Browse files
authored
Merge pull request #166 from RevEngAI/maginflorian/plu-242-display-better-error-message-when-users-try-to-upload
Check maximum permissible filesize before allowing analysis creation
2 parents c988a0f + 1194acf commit 95aa52a

6 files changed

Lines changed: 135 additions & 81 deletions

File tree

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ dependencies {
6666
implementation 'org.json:json:20250107'
6767
implementation "com.google.guava:guava:33.2.0-jre"
6868
implementation group: 'com.fifesoft', name: 'rsyntaxtextarea', version: '3.5.2'
69-
implementation "ai.reveng:sdk:2.52.1"
69+
implementation('ai.reveng:sdk:3.0.0')
7070
testImplementation('junit:junit:4.13.1')
7171
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2")
7272

src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/analysiscreation/RevEngAIAnalysisOptionsDialog.java

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
package ai.reveng.toolkit.ghidra.binarysimilarity.ui.analysiscreation;
22

3+
import ai.reveng.model.ConfigResponse;
34
import ai.reveng.toolkit.ghidra.binarysimilarity.ui.dialog.RevEngDialogComponentProvider;
45
import ai.reveng.toolkit.ghidra.core.services.api.AnalysisOptionsBuilder;
56
import ai.reveng.toolkit.ghidra.core.services.api.GhidraRevengService;
67
import ai.reveng.toolkit.ghidra.core.services.api.types.AnalysisScope;
78
import ai.reveng.toolkit.ghidra.plugins.ReaiPluginPackage;
89
import ghidra.program.model.listing.Program;
10+
import ghidra.util.Msg;
11+
import ghidra.util.Swing;
912

1013
import javax.annotation.Nullable;
1114
import javax.swing.*;
1215
import java.awt.*;
16+
import java.io.IOException;
17+
import java.nio.file.Files;
18+
import java.nio.file.InvalidPathException;
19+
import java.nio.file.Path;
1320
import java.util.List;
21+
import java.util.concurrent.CompletableFuture;
1422

1523
public class RevEngAIAnalysisOptionsDialog extends RevEngDialogComponentProvider {
1624
private JCheckBox advancedAnalysisCheckBox;
1725
private JCheckBox dynamicExecutionCheckBox;
1826
private final Program program;
27+
private final GhidraRevengService service;
1928
private JRadioButton privateScope;
2029
private JRadioButton publicScope;
2130
private JTextField tagsTextBox;
@@ -26,16 +35,22 @@ public class RevEngAIAnalysisOptionsDialog extends RevEngDialogComponentProvider
2635
private JComboBox<String> architectureComboBox;
2736
private boolean okPressed = false;
2837

38+
private JLabel fileSizeWarningLabel;
39+
private JLabel loadingLabel;
40+
2941
public static RevEngAIAnalysisOptionsDialog withModelsFromServer(Program program, GhidraRevengService reService) {
30-
return new RevEngAIAnalysisOptionsDialog(program);
42+
return new RevEngAIAnalysisOptionsDialog(program, reService);
3143
}
3244

33-
public RevEngAIAnalysisOptionsDialog(Program program) {
45+
public RevEngAIAnalysisOptionsDialog(Program program, GhidraRevengService service) {
3446
super(ReaiPluginPackage.WINDOW_PREFIX + "Configure Analysis for %s".formatted(program.getName()), true);
3547
this.program = program;
48+
this.service = service;
3649

3750
buildInterface();
38-
setPreferredSize(320, 380);
51+
setPreferredSize(320, 420);
52+
53+
fetchConfigAsync();
3954
}
4055

4156
private void buildInterface() {
@@ -48,6 +63,15 @@ private void buildInterface() {
4863
JPanel titlePanel = createTitlePanel("Create new analysis for this binary");
4964
workPanel.add(titlePanel, BorderLayout.NORTH);
5065

66+
// File size warning label (hidden by default)
67+
fileSizeWarningLabel = new JLabel();
68+
fileSizeWarningLabel.setForeground(Color.RED);
69+
fileSizeWarningLabel.setHorizontalAlignment(SwingConstants.CENTER);
70+
fileSizeWarningLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
71+
fileSizeWarningLabel.setVisible(false);
72+
fileSizeWarningLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
73+
workPanel.add(fileSizeWarningLabel);
74+
5175
// Add Platform Drop Down
5276
var platformComboBox = new JComboBox<>(new String[]{
5377
"Auto", "windows", "linux",
@@ -144,10 +168,19 @@ private void buildInterface() {
144168
workPanel.add(tagsLabel);
145169
workPanel.add(tagsTextBox);
146170

171+
// Loading indicator (shown while fetching config)
172+
loadingLabel = new JLabel("Checking file size limits...");
173+
loadingLabel.setForeground(Color.GRAY);
174+
loadingLabel.setHorizontalAlignment(SwingConstants.CENTER);
175+
loadingLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
176+
loadingLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
177+
workPanel.add(loadingLabel);
178+
147179
addCancelButton();
148180
addOKButton();
149181

150182
okButton.setText("Start Analysis");
183+
okButton.setEnabled(false); // Disabled until config check completes
151184
}
152185

153186
public @Nullable AnalysisOptionsBuilder getOptionsFromUI() {
@@ -187,4 +220,80 @@ protected void okCallback() {
187220
public JComponent getComponent() {
188221
return super.getComponent();
189222
}
223+
224+
private void fetchConfigAsync() {
225+
CompletableFuture.supplyAsync(() -> {
226+
try {
227+
return service.getApi().getConfig();
228+
} catch (Exception e) {
229+
Msg.warn(this, "Failed to fetch server config: " + e.getMessage());
230+
return null;
231+
}
232+
}).thenAccept(config -> {
233+
Swing.runNow(() -> handleConfigResponse(config));
234+
});
235+
}
236+
237+
private void handleConfigResponse(@Nullable ConfigResponse config) {
238+
loadingLabel.setVisible(false);
239+
240+
if (config == null) {
241+
// Config fetch failed, allow upload attempt (server will reject if too large)
242+
okButton.setEnabled(true);
243+
return;
244+
}
245+
246+
long maxFileSizeBytes = config.getMaxFileSizeBytes().longValue();
247+
validateFileSize(maxFileSizeBytes);
248+
}
249+
250+
private void validateFileSize(long maxFileSizeBytes) {
251+
long fileSize = getProgramFileSize();
252+
if (fileSize < 0) {
253+
// Could not determine file size, allow upload attempt
254+
okButton.setEnabled(true);
255+
return;
256+
}
257+
258+
if (fileSize > maxFileSizeBytes) {
259+
String fileSizeStr = formatBytes(fileSize);
260+
String maxSizeStr = formatBytes(maxFileSizeBytes);
261+
fileSizeWarningLabel.setText(
262+
"<html><center>File size (%s) exceeds<br>server limit (%s)</center></html>"
263+
.formatted(fileSizeStr, maxSizeStr));
264+
fileSizeWarningLabel.setVisible(true);
265+
okButton.setEnabled(false);
266+
} else {
267+
fileSizeWarningLabel.setVisible(false);
268+
okButton.setEnabled(true);
269+
}
270+
}
271+
272+
private long getProgramFileSize() {
273+
try {
274+
Path filePath;
275+
try {
276+
filePath = Path.of(program.getExecutablePath());
277+
} catch (InvalidPathException e) {
278+
// Windows paths may have leading slash like "/C:/file.dll"
279+
filePath = Path.of(program.getExecutablePath().substring(1));
280+
}
281+
return Files.size(filePath);
282+
} catch (IOException | InvalidPathException e) {
283+
Msg.warn(this, "Could not determine file size: " + e.getMessage());
284+
return -1;
285+
}
286+
}
287+
288+
private static String formatBytes(long bytes) {
289+
if (bytes < 1024) {
290+
return bytes + " B";
291+
} else if (bytes < 1024 * 1024) {
292+
return "%.1f KB".formatted(bytes / 1024.0);
293+
} else if (bytes < 1024 * 1024 * 1024) {
294+
return "%.1f MB".formatted(bytes / (1024.0 * 1024));
295+
} else {
296+
return "%.1f GB".formatted(bytes / (1024.0 * 1024 * 1024));
297+
}
298+
}
190299
}

src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import ai.reveng.api.*;
44
import ai.reveng.model.*;
5+
import ai.reveng.model.ConfigResponse;
56
import ai.reveng.toolkit.ghidra.core.services.api.types.*;
67
import ai.reveng.toolkit.ghidra.core.services.api.types.Collection;
78
import ai.reveng.toolkit.ghidra.core.services.api.types.FunctionMatch;
@@ -48,6 +49,7 @@
4849
public class TypedApiImplementation implements TypedApiInterface {
4950
private final HttpClient httpClient;
5051
private final String baseUrl;
52+
private final ConfigApi configApi;
5153
Map<String, String> headers;
5254

5355
private final AnalysesCoreApi analysisCoreApi;
@@ -101,6 +103,7 @@ public TypedApiImplementation(String baseUrl, String apiKey) {
101103
this.functionsRenamingHistoryApi = new FunctionsRenamingHistoryApi(apiClient);
102104
this.functionsAiDecompilationApi = new FunctionsAiDecompilationApi(apiClient);
103105
this.functionsDataTypesApi = new FunctionsDataTypesApi(apiClient);
106+
this.configApi = new ConfigApi(apiClient);
104107

105108
this.baseUrl = baseUrl + "/";
106109
this.httpClient = HttpClient.newBuilder()
@@ -666,5 +669,13 @@ public List<String> getAssembly(FunctionID id) {
666669
return result;
667670
}
668671

672+
@Override
673+
public ConfigResponse getConfig() {
674+
try {
675+
return this.configApi.getConfig().getData();
676+
} catch (ApiException e) {
677+
throw new RuntimeException(e);
678+
}
679+
}
669680
}
670681

src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
*
2929
*/
3030
public interface TypedApiInterface {
31+
3132
/// Data type to represent the RevEng.AI API concept of a function ID
3233
record FunctionID(long value){
3334
public Integer asInteger() {
@@ -225,5 +226,8 @@ default List<String> getAssembly(FunctionID functionID) throws ApiException {
225226
throw new UnsupportedOperationException("getAssembly not implemented yet");
226227
}
227228

229+
default ConfigResponse getConfig() throws ApiException {
230+
throw new UnsupportedOperationException("getConfig not implemented yet");
231+
}
228232
}
229233

src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/ConfigResponse.java

Lines changed: 0 additions & 73 deletions
This file was deleted.

src/test/java/ai/reveng/AnalysisOptionsDialogTest.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717

1818
import static org.junit.Assert.*;
1919

20-
import java.util.*;
21-
2220
import javax.swing.*;
2321

2422
import ai.reveng.toolkit.ghidra.binarysimilarity.ui.analysiscreation.RevEngAIAnalysisOptionsDialog;
@@ -41,17 +39,22 @@ public AnalysisOptionsDialogTest() {
4139
}
4240

4341
@Test
44-
public void testWithMockModels() throws Exception {
42+
public void testBasicOptionsDialog() throws Exception {
4543

4644
var reService = new GhidraRevengService( new MockApi() {});
4745
var builder = new ProgramBuilder("mock", ProgramBuilder._X64, this);
48-
4946
var program = builder.getProgram();
5047
var dialog = RevEngAIAnalysisOptionsDialog.withModelsFromServer(program, reService);
5148
SwingUtilities.invokeLater(() -> {
5249
DockingWindowManager.showDialog(null, dialog);
5350
});
5451
waitForSwing();
52+
waitFor(
53+
() -> {
54+
JButton okButton = (JButton) getInstanceField("okButton", dialog);
55+
return okButton.isEnabled();
56+
}
57+
);
5558
runSwing(() -> {
5659
JButton okButton = (JButton) getInstanceField("okButton", dialog);
5760
okButton.doClick();

0 commit comments

Comments
 (0)