mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-10 06:02:09 +00:00
Merge remote-tracking branch 'origin/Ghidra_11.2'
This commit is contained in:
commit
593c12653d
@ -8,7 +8,8 @@
|
||||
</HEAD>
|
||||
|
||||
<BODY>
|
||||
<H1><A name="FunctionComparisonPlugin"></A> <A name="Function_Comparison"></A> <A name=
|
||||
<A name="Function_Comparison"></A>
|
||||
<H1><A name="FunctionComparisonPlugin"></A> <A name=
|
||||
"FunctionComparison"></A> Function Comparison Window</H1>
|
||||
|
||||
|
||||
@ -21,21 +22,18 @@
|
||||
</CENTER><BR>
|
||||
<BR>
|
||||
<BLOCKQUOTE>
|
||||
<A name="Function_Comparison_Actions"></A>
|
||||
<P>To Compare Functions, select one or more functions from the Listing, Decompiler or the
|
||||
<A HREF="help/topics/FunctionWindowPlugin/function_window.htm">Functions Table</A>, then
|
||||
right-click and select the <B>Compare Functions(s)</B> action (In the Listing, it is
|
||||
<B>Function <IMG src="help/shared/arrow.gif" border="0"> Compare Function(s)</B>).
|
||||
|
||||
<P>To begin, select a function (or multiple functions) from the listing or
|
||||
the <A HREF="help/topics/FunctionWindowPlugin/function_window.htm">function table</a>.
|
||||
Then right-click and select the <b>Compare Selected Functions</b> option.</P>
|
||||
|
||||
<P><A name="Dual_Listing"></A>A new function comparison window will appear (subsequent
|
||||
invocations of this option will create a new tab in the existing window).</P>
|
||||
<P><IMG src="help/shared/tip.png" border="0">If an existing function comparison window is
|
||||
already showing, the <B>Compare Function(s)</B> action will add the selected functions to
|
||||
the existing comparison. To get a new <B>Function Comparison</B> window, use the
|
||||
<B>Compare in New Window</B> action instead.</P>
|
||||
|
||||
<BLOCKQUOTE>
|
||||
<A name="Function_Comparison_Add_To"></A>
|
||||
<P><IMG src="help/shared/tip.png" border="0">Additional functions can be added to the
|
||||
last comparison window using the <I><B>Add To Last Comparison</B></I> popup action that will
|
||||
appear on components that can supply one or more functions. (Currently supported in the Listing
|
||||
and the Functions Window.)
|
||||
</P>
|
||||
</BLOCKQUOTE>
|
||||
</BLOCKQUOTE>
|
||||
|
||||
|
@ -125,10 +125,14 @@ public class ProgramLocationActionContext extends ProgramActionContext
|
||||
}
|
||||
|
||||
private Function getFunctionForLocation() {
|
||||
if (!(location instanceof FunctionLocation functionLocation)) {
|
||||
return null;
|
||||
}
|
||||
if (location instanceof FunctionLocation functionLocation) {
|
||||
Address functionAddress = functionLocation.getFunctionAddress();
|
||||
return program.getFunctionManager().getFunctionAt(functionAddress);
|
||||
}
|
||||
Address address = getAddress();
|
||||
if (address != null) {
|
||||
return program.getFunctionManager().getFunctionContaining(address);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -119,31 +119,37 @@ public class DataTypeIndexer {
|
||||
// which depended on how the binary search traversed the list. If there is a reason to use that
|
||||
// comparator over this one, then we need to re-think how this list is sorted.
|
||||
private class CaseInsensitiveDataTypeComparator implements Comparator<DataType> {
|
||||
|
||||
@Override
|
||||
public int compare(DataType dt1, DataType dt2) {
|
||||
String name1 = dt1.getName();
|
||||
String name2 = dt2.getName();
|
||||
|
||||
// if the names are the same, then sort by the path
|
||||
if (name1.equalsIgnoreCase(name2)) {
|
||||
|
||||
if (!name1.equals(name2)) {
|
||||
// let equivalent names be sorted by case ('-' for lower-case first)
|
||||
return -name1.compareTo(name2);
|
||||
int result = name1.compareToIgnoreCase(name2);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = name1.compareTo(name2);
|
||||
if (result != 0) {
|
||||
// let equivalent names be sorted by case ('-' for lower-case first)
|
||||
return -result;
|
||||
}
|
||||
|
||||
// if the names are the same, then sort by data type manager
|
||||
String dtmName1 = dt1.getDataTypeManager().getName();
|
||||
String dtmName2 = dt2.getDataTypeManager().getName();
|
||||
result = dtmName1.compareToIgnoreCase(dtmName2);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// if they have the same name, and are in the same DTM, then compare paths
|
||||
if (dtmName1.equalsIgnoreCase(dtmName2)) {
|
||||
return dt1.getPathName().compareToIgnoreCase(dt2.getPathName());
|
||||
}
|
||||
|
||||
return dtmName1.compareToIgnoreCase(dtmName2);
|
||||
}
|
||||
|
||||
return name1.compareToIgnoreCase(name2);
|
||||
CategoryPath cp1 = dt1.getCategoryPath();
|
||||
CategoryPath cp2 = dt2.getCategoryPath();
|
||||
String p1 = cp1.getPath();
|
||||
String p2 = cp2.getPath();
|
||||
return p1.compareToIgnoreCase(p2);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,9 @@ public class ConvertToSignedHexAction extends AbstractConvertAction {
|
||||
|
||||
@Override
|
||||
protected String getMenuName(Program program, Scalar scalar, boolean isData) {
|
||||
return getStandardLengthString("Signed Hex:") + convertToString(program, scalar, isData);
|
||||
return isData
|
||||
? null
|
||||
: getStandardLengthString("Signed Hex:") + convertToString(program, scalar, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -28,10 +28,10 @@ import ghidra.util.datastruct.Duo.Side;
|
||||
/**
|
||||
* Basic FunctionComparisonModel where a set of functions can be compared with each other
|
||||
*/
|
||||
public class DefaultFunctionComparisonModel extends AbstractFunctionComparisonModel {
|
||||
public class AnyToAnyFunctionComparisonModel extends AbstractFunctionComparisonModel {
|
||||
private Set<Function> functions = new HashSet<>();
|
||||
|
||||
public DefaultFunctionComparisonModel(Collection<Function> functions) {
|
||||
public AnyToAnyFunctionComparisonModel(Collection<Function> functions) {
|
||||
this.functions.addAll(functions);
|
||||
List<Function> orderedFunctions = getOrderedFunctions();
|
||||
if (orderedFunctions.size() == 1) {
|
||||
@ -44,7 +44,7 @@ public class DefaultFunctionComparisonModel extends AbstractFunctionComparisonMo
|
||||
}
|
||||
}
|
||||
|
||||
public DefaultFunctionComparisonModel(Function... functions) {
|
||||
public AnyToAnyFunctionComparisonModel(Function... functions) {
|
||||
this(Arrays.asList(functions));
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import org.junit.Test;
|
||||
|
||||
import generic.test.AbstractGenericTest;
|
||||
import ghidra.app.services.FunctionComparisonService;
|
||||
import ghidra.features.base.codecompare.model.DefaultFunctionComparisonModel;
|
||||
import ghidra.features.base.codecompare.model.AnyToAnyFunctionComparisonModel;
|
||||
import ghidra.features.base.codecompare.model.FunctionComparisonModelListener;
|
||||
import ghidra.program.database.ProgramBuilder;
|
||||
import ghidra.program.model.data.ByteDataType;
|
||||
@ -41,10 +41,10 @@ import ghidra.util.datastruct.Duo.Side;
|
||||
* model directly.
|
||||
* <ul>
|
||||
* <li>The API methods being tested: {@link FunctionComparisonService}</li>
|
||||
* <li>The model being used for verification: {@link DefaultFunctionComparisonModel}</li>
|
||||
* <li>The model being used for verification: {@link AnyToAnyFunctionComparisonModel}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class DefaultComparisonModelTest extends AbstractGhidraHeadedIntegrationTest {
|
||||
public class AnyToAnyFunctionComparisonModelTest extends AbstractGhidraHeadedIntegrationTest {
|
||||
|
||||
private Program program1;
|
||||
private Program program2;
|
||||
@ -54,7 +54,7 @@ public class DefaultComparisonModelTest extends AbstractGhidraHeadedIntegrationT
|
||||
private Function b1;
|
||||
private Function b2;
|
||||
private Function b3;
|
||||
private DefaultFunctionComparisonModel model;
|
||||
private AnyToAnyFunctionComparisonModel model;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
@ -66,7 +66,7 @@ public class DefaultComparisonModelTest extends AbstractGhidraHeadedIntegrationT
|
||||
|
||||
@Test
|
||||
public void testSetNoFunctions() throws Exception {
|
||||
model = new DefaultFunctionComparisonModel(new HashSet<>());
|
||||
model = new AnyToAnyFunctionComparisonModel(new HashSet<>());
|
||||
assertTrue(model.isEmpty());
|
||||
assertEquals(0, model.getFunctions(LEFT).size());
|
||||
assertEquals(0, model.getFunctions(RIGHT).size());
|
||||
@ -77,7 +77,7 @@ public class DefaultComparisonModelTest extends AbstractGhidraHeadedIntegrationT
|
||||
@Test
|
||||
public void testSetOneFunctions() throws Exception {
|
||||
Set<Function> set = Set.of(b1);
|
||||
model = new DefaultFunctionComparisonModel(set);
|
||||
model = new AnyToAnyFunctionComparisonModel(set);
|
||||
|
||||
assertFalse(model.isEmpty());
|
||||
assertEquals(List.of(b1), model.getFunctions(LEFT));
|
||||
@ -89,7 +89,7 @@ public class DefaultComparisonModelTest extends AbstractGhidraHeadedIntegrationT
|
||||
@Test
|
||||
public void testPairOfFunctions() throws Exception {
|
||||
Set<Function> set = Set.of(b1, b2);
|
||||
model = new DefaultFunctionComparisonModel(set);
|
||||
model = new AnyToAnyFunctionComparisonModel(set);
|
||||
|
||||
assertEquals(List.of(b1, b2), model.getFunctions(LEFT));
|
||||
assertEquals(List.of(b1, b2), model.getFunctions(RIGHT));
|
||||
@ -235,7 +235,7 @@ public class DefaultComparisonModelTest extends AbstractGhidraHeadedIntegrationT
|
||||
@Test
|
||||
public void testSettingBadFunctionActive() {
|
||||
Set<Function> set = Set.of(a1, b1);
|
||||
model = new DefaultFunctionComparisonModel(set);
|
||||
model = new AnyToAnyFunctionComparisonModel(set);
|
||||
|
||||
assertEquals(a1, model.getActiveFunction(LEFT));
|
||||
model.setActiveFunction(LEFT, a3);
|
||||
@ -280,9 +280,9 @@ public class DefaultComparisonModelTest extends AbstractGhidraHeadedIntegrationT
|
||||
return builder;
|
||||
}
|
||||
|
||||
private DefaultFunctionComparisonModel createTestModel() {
|
||||
private AnyToAnyFunctionComparisonModel createTestModel() {
|
||||
Set<Function> set = Set.of(b1, b2, a1, a2);
|
||||
return new DefaultFunctionComparisonModel(set);
|
||||
return new AnyToAnyFunctionComparisonModel(set);
|
||||
}
|
||||
|
||||
private class TestFunctionComparisonModelListener implements FunctionComparisonModelListener {
|
@ -39,7 +39,7 @@ import ghidra.util.datastruct.Duo.Side;
|
||||
* call. There are a few tests that also exercise various features of the data
|
||||
* model directly.
|
||||
* <li>The API methods being tested: {@link FunctionComparisonService}</li>
|
||||
* <li>The model being used for verification: {@link DefaultFunctionComparisonModel}</li>
|
||||
* <li>The model being used for verification: {@link AnyToAnyFunctionComparisonModel}</li>
|
||||
*/
|
||||
public class MatchedFunctionComparisonModelTest extends AbstractGhidraHeadedIntegrationTest {
|
||||
|
||||
|
@ -21,11 +21,12 @@ import java.util.function.Consumer;
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.app.CorePluginPackage;
|
||||
import ghidra.app.context.FunctionSupplierContext;
|
||||
import ghidra.app.context.ListingActionContext;
|
||||
import ghidra.app.events.*;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.app.plugin.ProgramPlugin;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.features.base.codecompare.model.DefaultFunctionComparisonModel;
|
||||
import ghidra.app.services.FunctionComparisonService;
|
||||
import ghidra.features.base.codecompare.model.AnyToAnyFunctionComparisonModel;
|
||||
import ghidra.features.base.codecompare.model.FunctionComparisonModel;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.plugintool.PluginInfo;
|
||||
@ -62,9 +63,8 @@ import utility.function.Callback;
|
||||
public class FunctionComparisonPlugin extends ProgramPlugin
|
||||
implements DomainObjectListener, FunctionComparisonService {
|
||||
|
||||
// Keep a stack of recently added providers so that the "add to comparison" service methods
|
||||
// can easily add to the last created provider.
|
||||
private Deque<FunctionComparisonProvider> providers = new ArrayDeque<>();
|
||||
private Set<FunctionComparisonProvider> providers = new HashSet<>();
|
||||
private FunctionComparisonProvider lastActiveProvider;
|
||||
|
||||
public FunctionComparisonPlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
@ -116,12 +116,21 @@ public class FunctionComparisonPlugin extends ProgramPlugin
|
||||
|
||||
void providerClosed(FunctionComparisonProvider provider) {
|
||||
providers.remove(provider);
|
||||
if (lastActiveProvider == provider) {
|
||||
lastActiveProvider = null;
|
||||
}
|
||||
}
|
||||
|
||||
void removeFunction(Function function) {
|
||||
Swing.runIfSwingOrRunLater(() -> doRemoveFunction(function));
|
||||
}
|
||||
|
||||
void providerActivated(FunctionComparisonProvider provider) {
|
||||
if (provider.supportsAddingFunctions()) {
|
||||
lastActiveProvider = provider;
|
||||
}
|
||||
}
|
||||
|
||||
private void foreEachProvider(Consumer<FunctionComparisonProvider> c) {
|
||||
// copy needed because this may cause callbacks to remove a provider from our list
|
||||
List<FunctionComparisonProvider> localCopy = new ArrayList<>(providers);
|
||||
@ -134,25 +143,60 @@ public class FunctionComparisonPlugin extends ProgramPlugin
|
||||
}
|
||||
|
||||
private void createActions() {
|
||||
new ActionBuilder("Compare Functions", getName())
|
||||
.description("Create Function Comparison")
|
||||
|
||||
HelpLocation help = new HelpLocation("FunctionComparison", "Function_Comparison_Actions");
|
||||
|
||||
new ActionBuilder("Function Comparison", getName())
|
||||
.popupMenuPath("Compare Function(s)")
|
||||
.helpLocation(new HelpLocation("FunctionComparison", "Function_Comparison"))
|
||||
.popupMenuGroup("Functions", "Z1")
|
||||
.description("Adds the selected function(s) to the current comparison window.")
|
||||
.helpLocation(help)
|
||||
.withContext(FunctionSupplierContext.class)
|
||||
.enabledWhen(c -> c.hasFunctions())
|
||||
.enabledWhen(c -> !isListing(c) && c.hasFunctions())
|
||||
.onAction(c -> addToComparison(c.getFunctions()))
|
||||
.buildAndInstall(tool);
|
||||
|
||||
// same action as above, but with an extra pull right when shown in the listing
|
||||
new ActionBuilder("Function Comparison (Listing)", getName())
|
||||
.popupMenuPath("Function", "Compare Function(s)")
|
||||
.popupMenuGroup("Functions", "Z1")
|
||||
.description("Adds the selected function(s) to the current comparison window.")
|
||||
.helpLocation(help)
|
||||
.withContext(FunctionSupplierContext.class)
|
||||
.enabledWhen(c -> isListing(c) && c.hasFunctions())
|
||||
.onAction(c -> addToComparison(c.getFunctions()))
|
||||
.buildAndInstall(tool);
|
||||
|
||||
new ActionBuilder("New Function Comparison", getName())
|
||||
.popupMenuPath("Compare in New Window")
|
||||
.popupMenuGroup("Functions", "Z2")
|
||||
.description("Compare the selected function(s) in a new comparison window.")
|
||||
.helpLocation(help)
|
||||
.withContext(FunctionSupplierContext.class)
|
||||
.enabledWhen(
|
||||
c -> !isListing(c) && c.hasFunctions() && hasExistingComparison())
|
||||
.onAction(c -> createComparison(c.getFunctions()))
|
||||
.buildAndInstall(tool);
|
||||
|
||||
new ActionBuilder("Add To Last Function Comparison", getName())
|
||||
.description("Add the selected function(s) to the last Function Comparison window")
|
||||
.popupMenuPath("Add To Last Comparison")
|
||||
.helpLocation(new HelpLocation("FunctionComparison", "Function_Comparison_Add_To"))
|
||||
// same action as above, but with an extra pull right when shown in the listing
|
||||
new ActionBuilder("New Function Comparison (Listing)", getName())
|
||||
.popupMenuPath("Function", "Compare in New Window")
|
||||
.popupMenuGroup("Functions", "Z2")
|
||||
.description("Compare the selected function(s) in a new comparison window.")
|
||||
.helpLocation(help)
|
||||
.withContext(FunctionSupplierContext.class)
|
||||
.enabledWhen(c -> c.hasFunctions())
|
||||
.onAction(c -> addToComparison(c.getFunctions()))
|
||||
.enabledWhen(c -> isListing(c) && c.hasFunctions() && hasExistingComparison())
|
||||
.onAction(c -> createComparison(c.getFunctions()))
|
||||
.buildAndInstall(tool);
|
||||
|
||||
}
|
||||
|
||||
private boolean isListing(FunctionSupplierContext context) {
|
||||
return context instanceof ListingActionContext;
|
||||
}
|
||||
|
||||
private boolean hasExistingComparison() {
|
||||
return lastActiveProvider != null;
|
||||
}
|
||||
|
||||
private void doRemoveFunction(Function function) {
|
||||
@ -168,20 +212,10 @@ public class FunctionComparisonPlugin extends ProgramPlugin
|
||||
FunctionComparisonProvider provider =
|
||||
new FunctionComparisonProvider(this, model, closeListener);
|
||||
|
||||
// insert at the top so the last created provider is first when searching for a provider
|
||||
providers.addFirst(provider);
|
||||
providers.add(provider);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private FunctionComparisonProvider findLastDefaultProviderModel() {
|
||||
for (FunctionComparisonProvider provider : providers) {
|
||||
if (provider.getModel() instanceof DefaultFunctionComparisonModel) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Service Methods
|
||||
//==================================================================================================
|
||||
@ -190,26 +224,23 @@ public class FunctionComparisonPlugin extends ProgramPlugin
|
||||
if (functions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
DefaultFunctionComparisonModel model = new DefaultFunctionComparisonModel(functions);
|
||||
AnyToAnyFunctionComparisonModel model = new AnyToAnyFunctionComparisonModel(functions);
|
||||
Swing.runLater(() -> createProvider(model));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createComparison(Function left, Function right) {
|
||||
DefaultFunctionComparisonModel model = new DefaultFunctionComparisonModel(left, right);
|
||||
AnyToAnyFunctionComparisonModel model = new AnyToAnyFunctionComparisonModel(left, right);
|
||||
Swing.runLater(() -> createProvider(model));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToComparison(Collection<Function> functions) {
|
||||
FunctionComparisonProvider lastProvider = findLastDefaultProviderModel();
|
||||
if (lastProvider == null) {
|
||||
if (lastActiveProvider == null) {
|
||||
createComparison(functions);
|
||||
}
|
||||
else {
|
||||
DefaultFunctionComparisonModel model =
|
||||
(DefaultFunctionComparisonModel) lastProvider.getModel();
|
||||
Swing.runLater(() -> model.addFunctions(functions));
|
||||
Swing.runLater(() -> lastActiveProvider.addFunctions(functions));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -299,7 +299,7 @@ public class FunctionComparisonProvider extends ComponentProviderAdapter
|
||||
.onAction(c -> maybeGoToActiveFunction())
|
||||
.buildAndInstallLocal(this);
|
||||
|
||||
if (model instanceof DefaultFunctionComparisonModel) {
|
||||
if (model instanceof AnyToAnyFunctionComparisonModel) {
|
||||
createDefaultModelActions();
|
||||
}
|
||||
}
|
||||
@ -313,7 +313,7 @@ public class FunctionComparisonProvider extends ComponentProviderAdapter
|
||||
.popupMenuGroup(ADD_COMPARISON_GROUP)
|
||||
.toolBarIcon(ADD_TO_COMPARISON_ICON)
|
||||
.toolBarGroup(ADD_COMPARISON_GROUP)
|
||||
.enabledWhen(c -> model instanceof DefaultFunctionComparisonModel)
|
||||
.enabledWhen(c -> model instanceof AnyToAnyFunctionComparisonModel)
|
||||
.onAction(c -> addFunctions())
|
||||
.buildAndInstallLocal(this);
|
||||
|
||||
@ -335,7 +335,7 @@ public class FunctionComparisonProvider extends ComponentProviderAdapter
|
||||
List<Function> functions =
|
||||
rows.stream().map(row -> row.getFunction()).collect(Collectors.toList());
|
||||
|
||||
if (model instanceof DefaultFunctionComparisonModel defaultModel) {
|
||||
if (model instanceof AnyToAnyFunctionComparisonModel defaultModel) {
|
||||
defaultModel.addFunctions(functions);
|
||||
}
|
||||
|
||||
@ -388,4 +388,29 @@ public class FunctionComparisonProvider extends ComponentProviderAdapter
|
||||
closeListener = Callback.dummy();
|
||||
functionComparisonPanel.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentActivated() {
|
||||
plugin.providerActivated(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this provider is using the {@link AnyToAnyFunctionComparisonModel} which
|
||||
* allows adding functions. The other model ({@link MatchedFunctionComparisonModel} ) only
|
||||
* allows functions to be added in matched pairs.
|
||||
* @return true if this provider supports adding functions to the comparison
|
||||
*/
|
||||
public boolean supportsAddingFunctions() {
|
||||
return model instanceof AnyToAnyFunctionComparisonModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds functions to the comparison model if the model supports it.
|
||||
* @param functions the functions to add to the comparison
|
||||
*/
|
||||
public void addFunctions(Collection<Function> functions) {
|
||||
if (model instanceof AnyToAnyFunctionComparisonModel anyToAnyModel) {
|
||||
anyToAnyModel.addFunctions(functions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
Loading…
Reference in New Issue
Block a user