mirror of
https://github.com/NationalSecurityAgency/ghidra
synced 2026-08-22 18:32:31 -04:00
GP-7137 - Updated the register picker and the Register Provider to use a
natural sort
This commit is contained in:
parent
34b446a328
commit
e728c307b7
7 changed files with 488 additions and 201 deletions
|
|
@ -41,6 +41,7 @@ import ghidra.program.model.listing.Program;
|
|||
import ghidra.program.model.listing.ProgramContext;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.Swing;
|
||||
import ghidra.util.datastruct.AlphaNumericComparator;
|
||||
|
||||
class VarnodeLocationCellEditor extends AbstractCellEditor
|
||||
implements TableCellEditor, FocusableEditor {
|
||||
|
|
@ -178,7 +179,11 @@ class VarnodeLocationCellEditor extends AbstractCellEditor
|
|||
|
||||
RegisterDropDownSelectionDataModel registerModel =
|
||||
new RegisterDropDownSelectionDataModel(registers);
|
||||
registerEntryTextField = new DropDownSelectionTextField<>(registerModel);
|
||||
|
||||
// A smaller min delay to make the drop-down more responsive. The number of registers
|
||||
// should not be large enough to make the UI sluggish as the user is typing.
|
||||
int minDelay = 200;
|
||||
registerEntryTextField = new DropDownSelectionTextField<>(registerModel, minDelay);
|
||||
registerEntryTextField.setBorder(null);
|
||||
|
||||
// this allows us to show the matching list when there is no text in the editor
|
||||
|
|
@ -230,7 +235,19 @@ class VarnodeLocationCellEditor extends AbstractCellEditor
|
|||
}
|
||||
}
|
||||
|
||||
Collections.sort(registers);
|
||||
Collections.sort(registers, new RegisterComparator());
|
||||
return registers;
|
||||
}
|
||||
|
||||
private static class RegisterComparator implements Comparator<Register> {
|
||||
|
||||
private AlphaNumericComparator alphaNumericComparator = new AlphaNumericComparator();
|
||||
|
||||
@Override
|
||||
public int compare(Register r1, Register r2) {
|
||||
String s1 = r1.getName().toLowerCase();
|
||||
String s2 = r2.getName().toLowerCase();
|
||||
return alphaNumericComparator.compare(s1, s2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
|
@ -28,6 +28,7 @@ import docking.widgets.tree.GTreeNode;
|
|||
import generic.theme.GIcon;
|
||||
import ghidra.program.model.lang.Register;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.datastruct.AlphaNumericComparator;
|
||||
import resources.Icons;
|
||||
|
||||
public class RegisterTree extends GTree {
|
||||
|
|
@ -81,9 +82,8 @@ public class RegisterTree extends GTree {
|
|||
list.add(reg);
|
||||
}
|
||||
}
|
||||
Collections.sort(list);
|
||||
Register[] registers = new Register[list.size()];
|
||||
return list.toArray(registers);
|
||||
|
||||
return list.toArray(Register[]::new);
|
||||
}
|
||||
|
||||
void setFiltered(boolean b) {
|
||||
|
|
@ -111,13 +111,6 @@ public class RegisterTree extends GTree {
|
|||
Register[] registers = program.getProgramContext().getRegistersWithValues();
|
||||
root.setRegisters(registers);
|
||||
selectRegister(currentRegister);
|
||||
|
||||
// TODO: old school - delete me?
|
||||
// runWhenTreeIsDone(new Runnable() {
|
||||
// public void run() {
|
||||
// selectRegister(currentRegister);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,181 +126,196 @@ public class RegisterTree extends GTree {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SearchableRegisterTreeNode extends GTreeNode {
|
||||
public GTreeNode findNode(Register register) {
|
||||
List<GTreeNode> allChildren = getChildren();
|
||||
for (GTreeNode child : allChildren) {
|
||||
if (!(child instanceof RegisterTreeNode)) {
|
||||
continue;
|
||||
}
|
||||
RegisterTreeNode node = (RegisterTreeNode) child;
|
||||
if (node.getRegister().equals(register)) {
|
||||
return node;
|
||||
}
|
||||
private static class RegisterNodeComparator implements Comparator<GTreeNode> {
|
||||
|
||||
GTreeNode foundNode = ((SearchableRegisterTreeNode) child).findNode(register);
|
||||
if (foundNode != null) {
|
||||
return foundNode;
|
||||
private AlphaNumericComparator alphaNumericComparator =
|
||||
new AlphaNumericComparator();
|
||||
|
||||
@Override
|
||||
public int compare(GTreeNode n1, GTreeNode n2) {
|
||||
String s1 = n1.getName().toLowerCase();
|
||||
String s2 = n2.getName().toLowerCase();
|
||||
return alphaNumericComparator.compare(s1, s2);
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class SearchableRegisterTreeNode extends GTreeNode {
|
||||
|
||||
private RegisterNodeComparator comparator = new RegisterNodeComparator();
|
||||
|
||||
void sortChildren() {
|
||||
List<GTreeNode> children = new ArrayList<>(children());
|
||||
Collections.sort(children, comparator);
|
||||
setChildren(children);
|
||||
|
||||
for (GTreeNode child : children) {
|
||||
SearchableRegisterTreeNode regNode = (SearchableRegisterTreeNode) child;
|
||||
regNode.sortChildren();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class RegisterTreeRootNode extends SearchableRegisterTreeNode {
|
||||
private Register[] lastRegisters;
|
||||
|
||||
@Override
|
||||
public Icon getIcon(boolean expanded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Registers";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTip() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setRegisters(Register[] registers) {
|
||||
if (registers == lastRegisters) {
|
||||
return;
|
||||
}
|
||||
removeAll(); // remove all current children before repopulating
|
||||
|
||||
lastRegisters = registers;
|
||||
HashMap<String, RegisterTreeGroupNode> groups =
|
||||
new HashMap<>();
|
||||
|
||||
List<GTreeNode> nodes = new ArrayList<>();
|
||||
|
||||
for (Register register : registers) {
|
||||
if (register.getBaseRegister() != register &&
|
||||
!register.getParentRegister().isHidden()) {
|
||||
continue;
|
||||
}
|
||||
String groupName = register.getGroup();
|
||||
if (groupName != null) {
|
||||
RegisterTreeGroupNode group = groups.get(groupName);
|
||||
if (group == null) {
|
||||
group = new RegisterTreeGroupNode(groupName);
|
||||
groups.put(groupName, group);
|
||||
nodes.add(group);
|
||||
public GTreeNode findNode(Register register) {
|
||||
List<GTreeNode> allChildren = getChildren();
|
||||
for (GTreeNode child : allChildren) {
|
||||
if (!(child instanceof RegisterTreeNode)) {
|
||||
continue;
|
||||
}
|
||||
RegisterTreeNode node = (RegisterTreeNode) child;
|
||||
if (node.getRegister().equals(register)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
GTreeNode foundNode = ((SearchableRegisterTreeNode) child).findNode(register);
|
||||
if (foundNode != null) {
|
||||
return foundNode;
|
||||
}
|
||||
group.addRegister(register);
|
||||
}
|
||||
else {
|
||||
nodes.add(new RegisterTreeNode(register));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RegisterTreeNode extends SearchableRegisterTreeNode {
|
||||
private static Icon REG_ICON = new GIcon("icon.plugin.register");
|
||||
private static Icon REG_GROUP_ICON = new GIcon("icon.plugin.register.provider");
|
||||
private final Register register;
|
||||
|
||||
public RegisterTreeNode(Register register) {
|
||||
this.register = register;
|
||||
for (Register childRegister : register.getChildRegisters()) {
|
||||
addNode(new RegisterTreeNode(childRegister));
|
||||
}
|
||||
}
|
||||
Collections.sort(nodes);
|
||||
setChildren(nodes);
|
||||
}
|
||||
}
|
||||
|
||||
class RegisterTreeNode extends SearchableRegisterTreeNode {
|
||||
private static Icon REG_ICON = new GIcon("icon.plugin.register");
|
||||
private static Icon REG_GROUP_ICON = new GIcon("icon.plugin.register.provider");
|
||||
private final Register register;
|
||||
|
||||
public RegisterTreeNode(Register register) {
|
||||
this.register = register;
|
||||
for (Register childRegister : register.getChildRegisters()) {
|
||||
addNode(new RegisterTreeNode(childRegister));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon(boolean expanded) {
|
||||
return register.hasChildren() ? REG_GROUP_ICON : REG_ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return register.getName() + " (" + register.getBitLength() + getAliases() + ")";
|
||||
}
|
||||
|
||||
private String getAliases() {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
for (String alias : register.getAliases()) {
|
||||
buf.append(buf.length() == 0 ? "; " : ", ");
|
||||
buf.append(alias);
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTip() {
|
||||
return register.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
return !register.hasChildren();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(GTreeNode other) {
|
||||
if (!(other instanceof RegisterTreeNode)) {
|
||||
return 1;
|
||||
}
|
||||
return getName().compareTo(other.getName());
|
||||
}
|
||||
|
||||
public Register getRegister() {
|
||||
return register;
|
||||
}
|
||||
}
|
||||
|
||||
class RegisterTreeGroupNode extends SearchableRegisterTreeNode {
|
||||
private static Icon OPEN_ICON = Icons.OPEN_FOLDER_ICON;
|
||||
private static Icon CLOSED_ICON = Icons.CLOSED_FOLDER_ICON;
|
||||
private String name;
|
||||
|
||||
public RegisterTreeGroupNode(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon(boolean expanded) {
|
||||
return expanded ? OPEN_ICON : CLOSED_ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTip() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addRegister(Register register) {
|
||||
addNode(new RegisterTreeNode(register));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(GTreeNode o) {
|
||||
if (!(o instanceof RegisterTreeGroupNode)) {
|
||||
return -1;
|
||||
}
|
||||
return name.compareTo(o.getName());
|
||||
|
||||
@Override
|
||||
public Icon getIcon(boolean expanded) {
|
||||
return register.hasChildren() ? REG_GROUP_ICON : REG_ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return register.getName() + " (" + register.getBitLength() + getAliases() +
|
||||
")";
|
||||
}
|
||||
|
||||
private String getAliases() {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
for (String alias : register.getAliases()) {
|
||||
buf.append(buf.length() == 0 ? "; " : ", ");
|
||||
buf.append(alias);
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTip() {
|
||||
return register.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
return !register.hasChildren();
|
||||
}
|
||||
|
||||
Register getRegister() {
|
||||
return register;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RegisterTreeGroupNode extends SearchableRegisterTreeNode {
|
||||
private static Icon OPEN_ICON = Icons.OPEN_FOLDER_ICON;
|
||||
private static Icon CLOSED_ICON = Icons.CLOSED_FOLDER_ICON;
|
||||
private String name;
|
||||
|
||||
public RegisterTreeGroupNode(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon(boolean expanded) {
|
||||
return expanded ? OPEN_ICON : CLOSED_ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTip() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addRegister(Register register) {
|
||||
addNode(new RegisterTreeNode(register));
|
||||
}
|
||||
}
|
||||
|
||||
private static class RegisterTreeRootNode extends SearchableRegisterTreeNode {
|
||||
private Register[] lastRegisters;
|
||||
|
||||
@Override
|
||||
public Icon getIcon(boolean expanded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Registers";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTip() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setRegisters(Register[] registers) {
|
||||
if (registers == lastRegisters) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeAll(); // remove all current children before re-populating
|
||||
|
||||
lastRegisters = registers;
|
||||
HashMap<String, RegisterTreeGroupNode> groups = new HashMap<>();
|
||||
|
||||
List<GTreeNode> nodes = new ArrayList<>();
|
||||
|
||||
for (Register register : registers) {
|
||||
if (register.getBaseRegister() != register &&
|
||||
!register.getParentRegister().isHidden()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String groupName = register.getGroup();
|
||||
if (groupName != null) {
|
||||
RegisterTreeGroupNode group = groups.get(groupName);
|
||||
if (group == null) {
|
||||
group = new RegisterTreeGroupNode(groupName);
|
||||
groups.put(groupName, group);
|
||||
nodes.add(group);
|
||||
}
|
||||
group.addRegister(register);
|
||||
}
|
||||
else {
|
||||
nodes.add(new RegisterTreeNode(register));
|
||||
}
|
||||
}
|
||||
|
||||
setChildren(nodes);
|
||||
sortChildren();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,15 +15,18 @@
|
|||
*/
|
||||
package ghidra.app.plugin.core.function.editor;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import generic.test.AbstractGenericTest;
|
||||
import ghidra.program.database.ProgramBuilder;
|
||||
import ghidra.program.database.ProgramDB;
|
||||
import ghidra.program.model.lang.Register;
|
||||
import ghidra.program.model.listing.ProgramContext;
|
||||
|
||||
public class VarnodeLocationCellEditorTest extends AbstractGenericTest {
|
||||
private static final int AARCH64_GENERAL_REGISTER_MAX = 30;
|
||||
|
|
@ -31,24 +34,23 @@ public class VarnodeLocationCellEditorTest extends AbstractGenericTest {
|
|||
@Test
|
||||
public void testAarch64XRegistersUseNumericOrder() throws Exception {
|
||||
ProgramBuilder builder = new ProgramBuilder("TestProgram", ProgramBuilder._AARCH64);
|
||||
try {
|
||||
List<Register> registers = VarnodeLocationCellEditor.getSortedVisibleRegisters(
|
||||
builder.getProgram().getProgramContext());
|
||||
List<String> xRegisters = new ArrayList<>();
|
||||
for (Register register : registers) {
|
||||
if (register.getName().matches("x\\d+")) {
|
||||
xRegisters.add(register.getName());
|
||||
}
|
||||
}
|
||||
ProgramDB p = builder.getProgram();
|
||||
builder.dispose();
|
||||
|
||||
List<String> expected = new ArrayList<>();
|
||||
for (int i = 0; i <= AARCH64_GENERAL_REGISTER_MAX; i++) {
|
||||
expected.add("x" + i);
|
||||
ProgramContext context = p.getProgramContext();
|
||||
List<Register> registers = VarnodeLocationCellEditor.getSortedVisibleRegisters(context);
|
||||
|
||||
List<String> xRegisters = new ArrayList<>();
|
||||
for (Register register : registers) {
|
||||
if (register.getName().matches("x\\d+")) {
|
||||
xRegisters.add(register.getName());
|
||||
}
|
||||
assertEquals(expected, xRegisters);
|
||||
}
|
||||
finally {
|
||||
builder.dispose();
|
||||
|
||||
List<String> expected = new ArrayList<>();
|
||||
for (int i = 0; i <= AARCH64_GENERAL_REGISTER_MAX; i++) {
|
||||
expected.add("x" + i);
|
||||
}
|
||||
assertEquals(expected, xRegisters);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
|
@ -48,6 +48,21 @@ public class DropDownSelectionTextField<T> extends DropDownTextField<T> {
|
|||
super(dataModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* This constructor allows you to control the delay between keystrokes before the items in the
|
||||
* view get updated. A reasonable delay allows users to type multiple characters before the UI
|
||||
* updates. For large data, this prevents the UI from feeling sluggish, since the UI is does
|
||||
* not have to process each keystroke. For small data, the UI can update in real time, which
|
||||
* feels responsive. The tradeoff is between feeling the most responsive vs adding a small
|
||||
* delay to prevent apparent freezing. The default value is #DEFAULT_MIN_UPDATE_DELAY.
|
||||
*
|
||||
* @param dataModel the data model
|
||||
* @param updateMinDelay the min update delay
|
||||
*/
|
||||
public DropDownSelectionTextField(DropDownTextFieldDataModel<T> dataModel, int updateMinDelay) {
|
||||
super(dataModel, updateMinDelay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListSelectionModel createListSelectionModel() {
|
||||
DefaultListSelectionModel model = new DefaultListSelectionModel();
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
|||
|
||||
private static final Cursor CURSOR_HAND = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
|
||||
private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor();
|
||||
protected static final int DEFAULT_MIN_UPDATE_DELAY = 350;
|
||||
private static final int DEFAULT_MAX_UPDATE_DELAY = 2000;
|
||||
private static final int MIN_HEIGHT = 300;
|
||||
private static final int MIN_WIDTH = 200;
|
||||
|
|
@ -136,7 +137,7 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
|||
* @param dataModel provides element storage and search capabilities to this component.
|
||||
*/
|
||||
public DropDownTextField(DropDownTextFieldDataModel<T> dataModel) {
|
||||
this(dataModel, 350);
|
||||
this(dataModel, DEFAULT_MIN_UPDATE_DELAY);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.util.datastruct;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* A comparator to perform a natural sort for text and numbers. Numbers in the text will compared
|
||||
* based on their parsed value, not their ASCII value.
|
||||
*/
|
||||
public class AlphaNumericComparator implements Comparator<String> {
|
||||
|
||||
@Override
|
||||
public int compare(String s1, String s2) {
|
||||
if (s1 == null || s2 == null) {
|
||||
return s1 == s2 ? 0 : (s1 == null ? -1 : 1);
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int len1 = s1.length();
|
||||
int len2 = s2.length();
|
||||
|
||||
// Core Pass: Compare semantic characters and mathematical numbers
|
||||
while (i < len1 && j < len2) {
|
||||
|
||||
char c1 = s1.charAt(i);
|
||||
char c2 = s2.charAt(j);
|
||||
|
||||
// Skip spaces dynamically
|
||||
if (c1 == ' ') {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c2 == ' ') {
|
||||
j++;
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean isDigit1 = Character.isDigit(c1);
|
||||
boolean isDigit2 = Character.isDigit(c2);
|
||||
if (isDigit1 && isDigit2) {
|
||||
// Parse full numbers from the current position
|
||||
long val1 = parseNumber(s1, i);
|
||||
long val2 = parseNumber(s2, j);
|
||||
if (val1 != val2) {
|
||||
return Long.compare(val1, val2);
|
||||
}
|
||||
|
||||
// Skip ahead past the digit sequences we just parsed
|
||||
i = skipDigits(s1, i);
|
||||
j = skipDigits(s2, j);
|
||||
}
|
||||
else {
|
||||
// Compare standard characters alphabetically
|
||||
if (c1 != c2) {
|
||||
return Character.compare(c1, c2);
|
||||
}
|
||||
i++;
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any remaining trailing spaces
|
||||
i = skipSpaces(s1, i);
|
||||
j = skipSpaces(s2, j);
|
||||
|
||||
// Tie-breaker 1: Shorter structure wins (e.g., "file4" before "file 4")
|
||||
boolean ended1 = (i == len1);
|
||||
boolean ended2 = (j == len2);
|
||||
if (ended1 != ended2) {
|
||||
return ended1 ? -1 : 1;
|
||||
}
|
||||
|
||||
// Both strings have the same length. If they have numbers, the numbers have been
|
||||
// parsed to be the same numeric value, but they may have leading zeros.
|
||||
// Prefer fewer leading zeros (e.g., "file4" before "file04").
|
||||
// A non-zero value means that both strings have digits that were compared.
|
||||
int lengthCompare = compareDigitLengths(s1, s2);
|
||||
if (lengthCompare != 0) {
|
||||
return lengthCompare;
|
||||
}
|
||||
|
||||
// Default to a string compare
|
||||
return s1.compareTo(s2);
|
||||
}
|
||||
|
||||
private long parseNumber(String s, int i) {
|
||||
long value = 0;
|
||||
while (i < s.length()) {
|
||||
char c = s.charAt(i);
|
||||
if (!Character.isDigit(c)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
int intValue = (c - '0');
|
||||
value = value * 10 + intValue;
|
||||
i++;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private int skipDigits(String s, int i) {
|
||||
int len = s.length();
|
||||
while (i < len && Character.isDigit(s.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
private int skipSpaces(String s, int i) {
|
||||
int len = s.length();
|
||||
while (i < len && s.charAt(i) == ' ') {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
private int compareDigitLengths(String s1, String s2) {
|
||||
int len1 = s1.length();
|
||||
int len2 = s2.length();
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
while (i < len1 && j < len2) {
|
||||
i = skipSpaces(s1, i);
|
||||
j = skipSpaces(s2, j);
|
||||
if (i >= len1 || j >= len2) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (Character.isDigit(s1.charAt(i)) && Character.isDigit(s2.charAt(j))) {
|
||||
int skip1 = skipDigits(s1, i) - i;
|
||||
int skip2 = skipDigits(s2, j) - j;
|
||||
if (skip1 != skip2) {
|
||||
return Integer.compare(skip1, skip2);
|
||||
}
|
||||
i += skip1;
|
||||
j += skip2;
|
||||
}
|
||||
else {
|
||||
i++;
|
||||
j++;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.util.datastruct;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import generic.test.AbstractGenericTest;
|
||||
|
||||
public class AlphaNumericComparatorTest extends AbstractGenericTest {
|
||||
|
||||
@Test
|
||||
public void testIt() {
|
||||
|
||||
List<String> unsorted = Arrays.asList(
|
||||
|
||||
" file10", // Leading spaces
|
||||
"file02 ", // Trailing spaces
|
||||
"file 2", // Internal spaces
|
||||
"file1", // Normal
|
||||
" file002 ", // Leading and trailing spaces
|
||||
"file 01", // Internal space before digit
|
||||
"file20",
|
||||
|
||||
" apple", // Leading spaces
|
||||
"Apple ", // Trailing spaces
|
||||
"ba nana", // Internal spaces
|
||||
"a",
|
||||
"alpha ", // Trailing spaces
|
||||
" alphabet", // Leading spaces
|
||||
"app",
|
||||
"ap ple", // Internal spaces
|
||||
|
||||
"0.1.0",
|
||||
"0.1.9",
|
||||
"1.0",
|
||||
"0.2.1",
|
||||
"2.1.0");
|
||||
|
||||
List<String> sorted = Arrays.asList(
|
||||
"0.1.0",
|
||||
"0.1.9",
|
||||
"0.2.1",
|
||||
"1.0",
|
||||
"2.1.0",
|
||||
"Apple ",
|
||||
"a",
|
||||
"alpha ",
|
||||
" alphabet",
|
||||
"app",
|
||||
" apple",
|
||||
"ap ple",
|
||||
"ba nana",
|
||||
"file1",
|
||||
"file 01",
|
||||
"file 2",
|
||||
"file02 ",
|
||||
" file002 ",
|
||||
" file10",
|
||||
"file20");
|
||||
|
||||
Collections.shuffle(unsorted);
|
||||
List<String> copy = new ArrayList<>(unsorted);
|
||||
Collections.sort(copy, new AlphaNumericComparator());
|
||||
|
||||
assertEquals(sorted, copy);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue