Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public class RevEngAIAnalysisOptionsDialog extends RevEngDialogComponentProvider
private JComboBox<String> architectureComboBox;
private boolean okPressed = false;

private boolean fileSizeOk = false;
private boolean tierResolved = false;

private FunctionSelectionTableModel functionSelectionModel;
private JTable functionSelectionTable;
private JLabel includeCountLabel;
Expand Down Expand Up @@ -108,7 +111,8 @@ private void buildInterface() {
scopePanel.setLayout(new BoxLayout(scopePanel, BoxLayout.X_AXIS));
privateScope = new JRadioButton("Private to you");
publicScope = new JRadioButton("Public access");
privateScope.setSelected(true);
publicScope.setSelected(true);
privateScope.setEnabled(false);

var group = new ButtonGroup();
group.add(privateScope);
Expand Down Expand Up @@ -227,7 +231,7 @@ private void buildInterface() {
options.advancedAnalysis(advancedAnalysisCheckBox.isSelected());
options.dynamicExecution(dynamicExecutionCheckBox.isSelected());

if (privateScope.isSelected()) {
if (privateScope.isSelected() && privateScope.isEnabled()) {
options.scope(AnalysisScope.PRIVATE);
} else {
options.scope(AnalysisScope.PUBLIC);
Expand Down Expand Up @@ -468,26 +472,38 @@ private void fetchUserTierAsync() {
}

private void handleUserResponse(@Nullable User user) {
if (user == null || user.getTier() != User.TierEnum.ENTHUSIAST) {
tierResolved = true;

if (user != null && user.getTier() != User.TierEnum.ENTHUSIAST) {
privateScope.setEnabled(true);
privateScope.setToolTipText(null);
privateScopePanel.setToolTipText(null);
privateScope.setSelected(true);
updateOkButtonState();
return;
}

String message = "<html>Private analyses are not available on the Enthusiast tier.<br>"
+ "Upgrade your plan to create private analyses.</html>";
String message = user == null
? "<html>Could not verify your subscription tier.<br>"
+ "Private analyses are unavailable until this can be confirmed.</html>"
: "<html>Private analyses are not available on the Enthusiast tier.<br>"
+ "Upgrade your plan to create private analyses.</html>";

privateScope.setSelected(false);
publicScope.setSelected(true);
privateScope.setEnabled(false);
privateScope.setToolTipText(message);
privateScopePanel.setToolTipText(message);
updateOkButtonState();
}

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);
fileSizeOk = true;
updateOkButtonState();
return;
}

Expand All @@ -499,7 +515,8 @@ private void validateFileSize(long maxFileSizeBytes) {
long fileSize = getProgramFileSize();
if (fileSize < 0) {
// Could not determine file size, allow upload attempt
okButton.setEnabled(true);
fileSizeOk = true;
updateOkButtonState();
return;
}

Expand All @@ -510,11 +527,16 @@ private void validateFileSize(long maxFileSizeBytes) {
"<html><center>File size (%s) exceeds<br>server limit (%s)</center></html>"
.formatted(fileSizeStr, maxSizeStr));
fileSizeWarningLabel.setVisible(true);
okButton.setEnabled(false);
fileSizeOk = false;
} else {
fileSizeWarningLabel.setVisible(false);
okButton.setEnabled(true);
fileSizeOk = true;
}
updateOkButtonState();
}

private void updateOkButtonState() {
okButton.setEnabled(fileSizeOk && tierResolved);
}

private long getProgramFileSize() {
Expand Down
125 changes: 125 additions & 0 deletions src/test/java/ai/reveng/AnalysisOptionsDialogTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

import static org.junit.Assert.*;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.swing.*;

import ai.reveng.model.User;
Expand Down Expand Up @@ -137,4 +140,126 @@ public User getMe() {
assertNotNull(options);
assertEquals(AnalysisScope.PRIVATE, options.getScope());
}

@Test
public void testPrivateScopeDisabledWhenTierFetchFails() throws Exception {
var reService = new GhidraRevengService(new MockApi() {
@Override
public User getMe() {
throw new RuntimeException("tier lookup failed");
}
});
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();

JRadioButton privateScope = (JRadioButton) getInstanceField("privateScope", dialog);
JRadioButton publicScope = (JRadioButton) getInstanceField("publicScope", dialog);

waitFor(() -> {
JButton okButton = (JButton) getInstanceField("okButton", dialog);
return okButton.isEnabled();
});

assertFalse("Private scope must fail safe to disabled when the tier can't be verified",
privateScope.isEnabled());
assertFalse(privateScope.isSelected());
assertTrue("Public scope must be selected when the tier can't be verified",
publicScope.isSelected());
assertNotNull("A disabled private scope must explain why on hover", privateScope.getToolTipText());

runSwing(() -> {
JButton okButton = (JButton) getInstanceField("okButton", dialog);
okButton.doClick();
});
var options = dialog.getOptionsFromUI();
assertNotNull(options);
assertEquals(AnalysisScope.PUBLIC, options.getScope());
}

@Test
public void testStartAnalysisBlockedUntilTierResolves() throws Exception {
CountDownLatch release = new CountDownLatch(1);
var reService = new GhidraRevengService(new MockApi() {
@Override
public User getMe() {
try {
release.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return new User().tier(User.TierEnum.REVERSER);
}
});
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();

JButton okButton = (JButton) getInstanceField("okButton", dialog);
JRadioButton privateScope = (JRadioButton) getInstanceField("privateScope", dialog);
JLabel loadingLabel = (JLabel) getInstanceField("loadingLabel", dialog);

// The file-size/config check resolves independently; wait for it so the only thing
// still holding the OK button disabled is the (blocked) tier fetch.
waitFor(() -> !loadingLabel.isVisible());

assertFalse("Start Analysis must stay disabled until the tier fetch resolves",
okButton.isEnabled());
assertFalse("Private must stay disabled until the tier fetch resolves",
privateScope.isEnabled());

release.countDown();

waitFor(okButton::isEnabled);
assertTrue(okButton.isEnabled());
assertTrue("Private becomes available once a non-enthusiast tier is confirmed",
privateScope.isEnabled());
}

@Test
public void testGetOptionsIgnoresDisabledPrivateSelection() throws Exception {
var reService = new GhidraRevengService(new MockApi() {
@Override
public User getMe() {
return new User().tier(User.TierEnum.ENTHUSIAST);
}
});
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();

JRadioButton privateScope = (JRadioButton) getInstanceField("privateScope", dialog);
waitFor(() -> !privateScope.isEnabled());

// Force a stale private selection onto the disabled control and confirm the submit-time
// guard still reports the analysis as public.
runSwing(() -> privateScope.setSelected(true));
assertTrue(privateScope.isSelected());
assertFalse(privateScope.isEnabled());

waitFor(() -> {
JButton okButton = (JButton) getInstanceField("okButton", dialog);
return okButton.isEnabled();
});
runSwing(() -> {
JButton okButton = (JButton) getInstanceField("okButton", dialog);
okButton.doClick();
});

var options = dialog.getOptionsFromUI();
assertNotNull(options);
assertEquals(AnalysisScope.PUBLIC, options.getScope());
}
}
Loading