diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/EnumEditorPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/EnumEditorPanel.java index 31d7e120b4..83254db835 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/EnumEditorPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/EnumEditorPanel.java @@ -28,12 +28,13 @@ import docking.widgets.OptionDialog; import docking.widgets.combobox.GhidraComboBox; import docking.widgets.label.GDLabel; import docking.widgets.label.GLabel; -import docking.widgets.table.GTableCellRenderer; -import docking.widgets.table.GTableTextCellEditor; +import docking.widgets.table.*; import docking.widgets.textfield.GValidatedTextField; import docking.widgets.textfield.GValidatedTextField.LongField.LongValidator; import docking.widgets.textfield.GValidatedTextField.ValidationFailedException; import docking.widgets.textfield.GValidatedTextField.ValidationMessageListener; +import generic.theme.Gui; +import ghidra.docking.settings.Settings; import ghidra.program.model.data.*; import ghidra.program.model.listing.DataTypeArchive; import ghidra.program.model.listing.Program; @@ -55,6 +56,7 @@ class EnumEditorPanel extends JPanel { private EnumDataType originalEnumDT; private EnumDataType editedEnumDT; + boolean showValuesAsHex = true; EnumEditorPanel(EnumDataType enumDT, EnumEditorProvider provider) { super(new BorderLayout()); @@ -298,11 +300,23 @@ class EnumEditorPanel extends JPanel { table.setRowHeight(table.getRowHeight() + 4); table.setDefaultEditor(String.class, new EnumStringCellEditor()); table.getColumnModel() - .getColumn(EnumTableModel.VALUE_COL) - .setCellEditor( - new EnumLongCellEditor()); + .getColumn(EnumTableModel.VALUE_COL) + .setCellEditor( + new EnumLongCellEditor()); table.setDefaultRenderer(String.class, new GTableCellRenderer()); + table.setDefaultRenderer(Long.class, new EnumValueRenderer()); add(createInfoPanel(), BorderLayout.SOUTH); + } + + private String getValueAsString(long value) { + if (showValuesAsHex) { + int length = editedEnumDT.getLength(); + if (editedEnumDT.isSigned()) { + return NumericUtilities.toSignedHexString(value); + } + return NumericUtilities.toHexString(value, length); + } + return Long.toString(value); } @@ -431,39 +445,22 @@ class EnumEditorPanel extends JPanel { private boolean validateNewLength(Integer length) { EnumDataType enuum = tableModel.getEnum(); - String[] names = enuum.getNames(); - for (String name : names) { - long value = enuum.getValue(name); - if (tableModel.isValueTooBigForLength(value, length)) { - vetoSizeChange(length, enuum.getLength(), value); - return false; - } + int minLength = enuum.getMinimumPossibleLength(); + if (length < minLength) { + vetoSizeChange(length, minLength, enuum.getLength()); + return false; } return true; } - private boolean validateNewValue(Long value) { - EnumDataType enuum = tableModel.getEnum(); - int length = enuum.getLength(); - return !tableModel.isValueTooBigForLength(value, length); - } - - private void vetoSizeChange(final int newLength, final int currentLength, final long badValue) { + private void vetoSizeChange(int newLength, int minLength, int currentLength) { Swing.runLater(() -> { - setStatusMessage("Enum size of " + newLength + " cannot contain the value " + "0x" + - Long.toHexString(badValue)); + setStatusMessage( + "Enum size of " + newLength + " is smaller than minimum enum size of " + minLength); sizeComboBox.setSelectedItem(Integer.valueOf(currentLength)); }); } - public String getValidValuesMessage() { - EnumDataType enuum = tableModel.getEnum(); - int length = enuum.getLength(); - long maxValue = length == 8 ? -1 : (1L << (8 * length)) - 1; - return "Valid values are from 0x0 to 0x" + Long.toHexString(maxValue); - - } - private void setFieldInfo(EnumDataType enuum) { nameField.setText(enuum.getDisplayName()); sizeComboBox.setSelectedItem(enuum.getLength()); @@ -482,6 +479,10 @@ class EnumEditorPanel extends JPanel { }); } + void setHexDisplayMode(boolean showHex) { + showValuesAsHex = showHex; + tableModel.fireTableDataChanged(); + } //================================================================================================== // Inner Classes //================================================================================================== @@ -494,12 +495,29 @@ class EnumEditorPanel extends JPanel { } public class RangeValidator extends LongValidator { + private long min; + private long max; + + public void setOriginalValue(long originalLong) { + EnumDataType enuum = tableModel.getEnum(); + EnumDataType copy = (EnumDataType) enuum.copy(enuum.getDataTypeManager()); + String name = copy.getName(originalLong); + copy.remove(name); + min = copy.getMinPossibleValue(); + max = copy.getMaxPossibleValue(); + } + @Override - public void validateLong(long oldLong, long newLong) throws ValidationFailedException { - if (!validateNewValue(newLong)) { - throw new ValidationFailedException(getValidValuesMessage()); + public void validateLong(long oldValue, long newValue) throws ValidationFailedException { + if (newValue < min || newValue > max) { + String minValue = getValueAsString(min); + String maxValue = getValueAsString(max); + String message = + "Valid values are in the range (" + minValue + ", " + maxValue + ")"; + throw new ValidationFailedException(message); } } + } public class StatusBarValidationMessageListener implements ValidationMessageListener { @@ -608,11 +626,46 @@ class EnumEditorPanel extends JPanel { } private class EnumLongCellEditor extends EnumCellEditor { + private RangeValidator validator; + public EnumLongCellEditor() { super(new GValidatedTextField.LongField(8)); GValidatedTextField f = (GValidatedTextField) getComponent(); - f.addValidator(new RangeValidator()); + validator = new RangeValidator(); + f.addValidator(validator); f.addValidationMessageListener(new StatusBarValidationMessageListener()); } + + @Override + public Component getTableCellEditorComponent(JTable table1, Object value, + boolean isSelected, int row, int column) { + Long longValue = (Long) value; + validator.setOriginalValue(longValue); + String s = getValueAsString(longValue); + return super.getTableCellEditorComponent(table1, s, isSelected, row, column); + } + } + + private class EnumValueRenderer extends GTableCellRenderer { + EnumValueRenderer() { + setFont(Gui.getFont("font.monospaced")); + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + JLabel renderer = (JLabel) super.getTableCellRendererComponent(data); + renderer.setHorizontalAlignment(SwingConstants.RIGHT); + return renderer; + } + + @Override + protected String formatNumber(Number value, Settings settings) { + if (value instanceof Long longValue) { + return getValueAsString(longValue); + } + return ""; + } + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/EnumEditorProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/EnumEditorProvider.java index 5d0220d841..1e837ea61d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/EnumEditorProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/EnumEditorProvider.java @@ -30,6 +30,7 @@ import org.apache.commons.lang3.StringUtils; import docking.ActionContext; import docking.ComponentProvider; import docking.action.*; +import docking.action.builder.ToggleActionBuilder; import docking.widgets.OptionDialog; import generic.theme.GIcon; import generic.theme.GThemeDefaults.Colors.Messages; @@ -80,6 +81,7 @@ public class EnumEditorProvider extends ComponentProviderAdapter private CategoryPath originalCategoryPath; private Enum originalEnum; private long originalEnumID = -1; + private ToggleDockingAction hexDisplayAction; /** * Construct a new enum editor provider. @@ -276,6 +278,14 @@ public class EnumEditorProvider extends ComponentProviderAdapter } private void createActions() { + hexDisplayAction = new ToggleActionBuilder("Toggle Hex Mode", plugin.getName()) + .menuPath("Show Enum Values in Hex") + .description("Toggles Enum value column to show values in hex or decimal") + .keyBinding("Shift-H") + .selected(true) + .onAction(c -> editorPanel.setHexDisplayMode(hexDisplayAction.isSelected())) + .buildAndInstallLocal(this); + addAction = new EnumPluginAction("Add Enum Value", e -> editorPanel.addEntry()); addAction.setEnabled(true); String editGroup = "Edit"; @@ -287,7 +297,7 @@ public class EnumEditorProvider extends ComponentProviderAdapter new EnumPluginAction("Delete Enum Value", e -> editorPanel.deleteSelectedEntries()); deleteAction.setEnabled(false); deleteAction - .setPopupMenuData(new MenuData(new String[] { "Delete" }, DELETE_ICON, editGroup)); + .setPopupMenuData(new MenuData(new String[] { "Delete" }, DELETE_ICON, editGroup)); deleteAction.setToolBarData(new ToolBarData(DELETE_ICON, editGroup)); deleteAction.setDescription("Delete the selected enum entries"); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/EnumTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/EnumTableModel.java index ec84cd9742..1ab36e88cb 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/EnumTableModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/EnumTableModel.java @@ -68,6 +68,9 @@ class EnumTableModel extends AbstractSortedTableModel { @Override public Class getColumnClass(int columnIndex) { + if (columnIndex == VALUE_COL) { + return Long.class; + } return String.class; } @@ -81,28 +84,8 @@ class EnumTableModel extends AbstractSortedTableModel { switch (columnIndex) { case NAME_COL: return v.getName(); - case VALUE_COL: - long mask; - - switch (enuum.getLength()) { - case 1: - mask = 0xffL; - break; - case 2: - mask = 0xffffL; - break; - case 4: - mask = 0xffffffffL; - break; - default: - case 8: - mask = 0xffffffffffffffffL; - break; - } - - return "0x" + Long.toHexString(v.getValue() & mask); - + return v.getValue(); case COMMENT_COL: return v.getComment(); } @@ -277,14 +260,15 @@ class EnumTableModel extends AbstractSortedTableModel { afterRow = 0; } long value = enumEntryList.get(afterRow).getValue() + 1; - if (isTooBig(value)) { + if (!isValidValue(value)) { value = 0; } boolean wrapOK = value != 0; while (enuum.getName(value) != null) { - if (isTooBig(++value)) { + if (!isValidValue(++value)) { if (wrapOK) { value = 0; + wrapOK = false; } else { break; @@ -294,17 +278,10 @@ class EnumTableModel extends AbstractSortedTableModel { return value; } - boolean isValueTooBigForLength(long value, int length) { - if (length < 8) { - long max = (1L << (8 * length)) - 1; - return value > max || value < 0; - } - return false; - } - - private boolean isTooBig(long value) { - int len = enuum.getLength(); - return isValueTooBigForLength(value, len); + private boolean isValidValue(long value) { + long min = enuum.getMinPossibleValue(); + long max = enuum.getMaxPossibleValue(); + return value >= min && value <= max; } private String getUniqueName() { diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/program/database/data/EnumDbTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/program/database/data/EnumDbTest.java new file mode 100644 index 0000000000..247e496dce --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/program/database/data/EnumDbTest.java @@ -0,0 +1,278 @@ +/* ### + * 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.program.database.data; + +import static org.junit.Assert.*; + +import org.junit.*; + +import ghidra.program.database.ProgramBuilder; +import ghidra.program.database.ProgramDB; +import ghidra.program.model.data.Enum; +import ghidra.program.model.data.EnumDataType; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; + +/** + * Tests for Enum data types. + */ +public class EnumDbTest extends AbstractGhidraHeadedIntegrationTest { + + private ProgramDB program; + private DataTypeManagerDB dataMgr; + private int transactionID; + private Enum enum1; + private Enum enum2; + private Enum enum4; + private Enum enum8; + + @Before + public void setUp() throws Exception { + program = createDefaultProgram("Test", ProgramBuilder._TOY, this); + + dataMgr = program.getDataTypeManager(); + transactionID = program.startTransaction("Test"); + enum1 = new EnumDataType("Test", 1); + enum2 = new EnumDataType("Test", 2); + enum4 = new EnumDataType("Test", 4); + enum8 = new EnumDataType("Test", 8); + + enum1 = (Enum) dataMgr.resolve(enum1, null); + enum2 = (Enum) dataMgr.resolve(enum2, null); + enum4 = (Enum) dataMgr.resolve(enum4, null); + enum8 = (Enum) dataMgr.resolve(enum8, null); + } + + @After + public void tearDown() throws Exception { + program.endTransaction(transactionID, false); + program.release(this); + } + + @Test + public void testCanAddHighUnsignedValue() { + enum1.add("a", 0xff); + assertEquals(0xff, enum1.getValue("a")); + } + + @Test + public void testCanAddNegativeValue() { + enum1.add("a", -1); + assertEquals(-1, enum1.getValue("a")); + } + + @Test + public void testCantAddNegativeAndHighUnsignedValue() { + enum1.add("a", -1); + try { + enum1.add("b", 0xff); + fail("Expected Illegal ArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testCantAddHighUnsignedAndNegativeValue() { + enum1.add("a", 0xff); + try { + enum1.add("b", -1); + fail("Expected Illegal ArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testCanAddNegativeAfterAddingThenRemovingHighUnsignedValue() { + enum1.add("a", 0xff); + enum1.remove("a"); + enum1.add("a", -1); + assertEquals(-1, enum1.getValue("a")); + } + + @Test + public void testGetMinPossibleWidthUnsigned() { + assertEquals(1, enum4.getMinimumPossibleLength()); + + enum4.add("a", 0); + assertEquals(1, enum4.getMinimumPossibleLength()); + + enum4.add("b", 0xff); + assertEquals(1, enum4.getMinimumPossibleLength()); + + enum4.add("c", 0x100); + assertEquals(2, enum4.getMinimumPossibleLength()); + + enum4.add("d", 0xffff); + assertEquals(2, enum4.getMinimumPossibleLength()); + + enum4.add("e", 0x1ffff); + assertEquals(4, enum4.getMinimumPossibleLength()); + + enum4.add("f", 0xffffffffL); + assertEquals(4, enum4.getMinimumPossibleLength()); + + } + + @Test + public void testGetMinPossibleWidthSigned() { + assertEquals(1, enum4.getMinimumPossibleLength()); + + enum4.add("a", 0); + assertEquals(1, enum4.getMinimumPossibleLength()); + + enum4.add("b", -0x1); + assertEquals(1, enum4.getMinimumPossibleLength()); + + enum4.add("c", 0xff); + assertEquals(2, enum4.getMinimumPossibleLength()); + + enum4.add("d", 0xffff); + assertEquals(4, enum4.getMinimumPossibleLength()); + + enum4.add("e", 0x1ffff); + assertEquals(4, enum4.getMinimumPossibleLength()); + + } + + @Test + public void testGetMinPossibleWidthSignedWithBigNegatives() { + assertEquals(1, enum4.getMinimumPossibleLength()); + + enum4.add("a", 0); + assertEquals(1, enum4.getMinimumPossibleLength()); + + enum4.add("b", -0x1); + assertEquals(1, enum4.getMinimumPossibleLength()); + + enum4.add("c", -0xff); + assertEquals(2, enum4.getMinimumPossibleLength()); + + enum4.add("d", -0xffff); + assertEquals(4, enum4.getMinimumPossibleLength()); + + enum4.add("e", -0x1ffff); + assertEquals(4, enum4.getMinimumPossibleLength()); + + } + + @Test + public void testMinMaxPossibleValuesNoSignednessSize1() { + assertEquals(-0x80, enum1.getMinPossibleValue()); + assertEquals(0xff, enum1.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesNoSignednessSize2() { + assertEquals(-0x8000, enum2.getMinPossibleValue()); + assertEquals(0xffff, enum2.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesNoSignednessSize4() { + assertEquals(-0x80000000L, enum4.getMinPossibleValue()); + assertEquals(0xffffffffL, enum4.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesNoSignednessSize8() { + assertEquals(Long.MIN_VALUE, enum8.getMinPossibleValue()); + assertEquals(Long.MAX_VALUE, enum8.getMaxPossibleValue()); + } + + public void testMinMaxPossibleValuesSignedSize1() { + enum1.add("a", -1); // this makes it an signed enum + assertEquals(-0x80, enum1.getMinPossibleValue()); + assertEquals(0x7f, enum1.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesSignedSize2() { + enum2.add("a", -1); // this makes it an signed enum + assertEquals(-0x8000, enum2.getMinPossibleValue()); + assertEquals(0x7fff, enum2.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesSignedSize4() { + enum4.add("a", -1); // this makes it an signed enum + assertEquals(-0x80000000L, enum4.getMinPossibleValue()); + assertEquals(0x7fffffffL, enum4.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesSignedSize8() { + enum8.add("a", -1); // this makes it an signed enum + assertEquals(Long.MIN_VALUE, enum8.getMinPossibleValue()); + assertEquals(Long.MAX_VALUE, enum8.getMaxPossibleValue()); + } + + public void testMinMaxPossibleValuesUnsignedSize1() { + enum1.add("a", 0xff); // this makes it an unsigned enum + assertEquals(0, enum1.getMinPossibleValue()); + assertEquals(0xff, enum1.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesUnsignedSize2() { + enum2.add("a", 0xffff); // this makes it an signed enum + assertEquals(0, enum2.getMinPossibleValue()); + assertEquals(0xffff, enum2.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesUnsignedSize4() { + enum4.add("a", 0xffffffffL); // this makes it an signed enum + assertEquals(0, enum4.getMinPossibleValue()); + assertEquals(0xffffffffL, enum4.getMaxPossibleValue()); + } + + @Test + public void testContainsName() { + enum4.add("a", 0x1); + enum4.add("b", -1); + assertTrue(enum4.contains("a")); + assertTrue(enum4.contains("b")); + assertFalse(enum4.contains("c")); + } + + @Test + public void testContainsValue() { + enum4.add("a", 0x1); + enum4.add("b", -1); + assertTrue(enum4.contains(-1)); + assertTrue(enum4.contains(1)); + assertFalse(enum4.contains(3)); + } + + @Test + public void testGetMinPossibleLengthFor8ByteEnum() { + enum8.add("a", -1); + assertEquals(1, enum8.getMinimumPossibleLength()); + } + + @Test + public void test8ByteEnumsAreSigned() { + enum8.add("a", 0); + assertEquals(Long.MIN_VALUE, enum8.getMinPossibleValue()); + assertEquals(Long.MAX_VALUE, enum8.getMaxPossibleValue()); + assertFalse(enum8.isSigned()); + enum8.add("b", -1); + assertTrue(enum8.isSigned()); + } +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/EnumDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/EnumDB.java index 93d9a0d5a3..1b1bc33bb3 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/EnumDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/EnumDB.java @@ -15,6 +15,8 @@ */ package ghidra.program.database.data; +import static ghidra.program.database.data.EnumSignedState.*; + import java.io.IOException; import java.math.BigInteger; import java.util.*; @@ -37,7 +39,6 @@ import ghidra.util.UniversalID; * Database implementation for the enumerated data type. */ class EnumDB extends DataTypeDB implements Enum { - private static final SettingsDefinition[] ENUM_SETTINGS_DEFINITIONS = new SettingsDefinition[] { MutabilitySettingsDefinition.DEF }; @@ -45,9 +46,10 @@ class EnumDB extends DataTypeDB implements Enum { private EnumValueDBAdapter valueAdapter; private Map nameMap; // name to value - private TreeMap> valueMap; // value to names + private SortedMap> valueMap; // value to names private Map commentMap; // name to comment private List bitGroups; + private EnumSignedState signedState = null; EnumDB(DataTypeManagerDB dataMgr, DBObjectCache cache, EnumDBAdapter adapter, EnumValueDBAdapter valueAdapter, DBRecord record) { @@ -97,6 +99,27 @@ class EnumDB extends DataTypeDB implements Enum { String comment = rec.getString(EnumValueDBAdapter.ENUMVAL_COMMENT_COL); addToCache(valueName, value, comment); } + signedState = computeSignedness(); + } + + private EnumSignedState computeSignedness() { + int length = record.getByteValue(EnumDBAdapter.ENUM_SIZE_COL); + + if (valueMap.isEmpty()) { + return NONE; + } + + long minValue = valueMap.firstKey(); + long maxValue = valueMap.lastKey(); + + if (minValue < 0) { + return SIGNED; + } + if (maxValue > getMaxPossibleValue(length, true)) { + return UNSIGNED; + } + + return NONE; // we have no negatives and no large unsigned values } private void addToCache(String valueName, long value, String comment) { @@ -258,8 +281,8 @@ class EnumDB extends DataTypeDB implements Enum { lock.acquire(); try { checkDeleted(); - checkValue(value); initializeIfNeeded(); + checkValue(value); if (nameMap.containsKey(valueName)) { throw new IllegalArgumentException(valueName + " already exists in this enum"); } @@ -272,6 +295,7 @@ class EnumDB extends DataTypeDB implements Enum { valueAdapter.createRecord(key, valueName, value, comment); adapter.updateRecord(record, true); addToCache(valueName, value, comment); + signedState = computeSignedness(); dataMgr.dataTypeChanged(this, false); } catch (IOException e) { @@ -283,17 +307,17 @@ class EnumDB extends DataTypeDB implements Enum { } private void checkValue(long value) { - int length = getLength(); + int length = record.getByteValue(EnumDBAdapter.ENUM_SIZE_COL); if (length == 8) { return; // all long values permitted } - // compute maximum enum value as a positive value: (2^length)-1 - long max = (1L << (getLength() * 8)) - 1; - if (value > max) { - throw new IllegalArgumentException( - getName() + " enum value 0x" + Long.toHexString(value) + - " is outside the range of 0x0 to 0x" + Long.toHexString(max)); + long min = getMinPossibleValue(); + long max = getMaxPossibleValue(); + if (value < min || value > max) { + throw new IllegalArgumentException( + "Attempted to add a value outside the range for this enum: (" + min + ", " + max + + "): " + value); } } @@ -317,6 +341,7 @@ class EnumDB extends DataTypeDB implements Enum { } } adapter.updateRecord(record, true); + signedState = computeSignedness(); dataMgr.dataTypeChanged(this, false); } catch (IOException e) { @@ -366,7 +391,7 @@ class EnumDB extends DataTypeDB implements Enum { adapter.updateRecord(record, true); addToCache(valueName, value, comment); } - + signedState = computeSignedness(); if (oldLength != newLength) { notifySizeChanged(false); } @@ -615,6 +640,56 @@ class EnumDB extends DataTypeDB implements Enum { } } + @Override + public long getMinPossibleValue() { + lock.acquire(); + try { + checkIsValid(); + int length = record.getByteValue(EnumDBAdapter.ENUM_SIZE_COL); + return getMinPossibleValue(length, signedState != UNSIGNED); + } + finally { + lock.release(); + } + } + + @Override + public long getMaxPossibleValue() { + lock.acquire(); + try { + checkIsValid(); + int length = record.getByteValue(EnumDBAdapter.ENUM_SIZE_COL); + return getMaxPossibleValue(length, signedState == SIGNED); + } + finally { + lock.release(); + } + } + + private long getMaxPossibleValue(int bytes, boolean allowNegativeValues) { + if (bytes == 8) { + return Long.MAX_VALUE; + } + int bits = bytes * 8; + if (allowNegativeValues) { + bits -= 1; // take away 1 bit for the sign + } + + // the largest value that can be held in n bits in 2^n -1 + return (1L << bits) - 1; + } + + private long getMinPossibleValue(int bytes, boolean allowNegativeValues) { + if (!allowNegativeValues) { + return 0; + } + int bits = bytes * 8; + + // smallest value (largest negative) that can be stored in n bits is when the sign bit + // is on (and sign extended), and all less significant bits are 0 + return -1L << (bits - 1); + } + @Override protected boolean refresh() { try { @@ -754,4 +829,71 @@ class EnumDB extends DataTypeDB implements Enum { lock.release(); } } + + @Override + public boolean contains(String name) { + lock.acquire(); + try { + checkIsValid(); + initializeIfNeeded(); + return nameMap.containsKey(name); + } + finally { + lock.release(); + } + } + + @Override + public boolean contains(long value) { + lock.acquire(); + try { + checkIsValid(); + initializeIfNeeded(); + return valueMap.containsKey(value); + } + finally { + lock.release(); + } + } + + @Override + public boolean isSigned() { + lock.acquire(); + try { + checkIsValid(); + initializeIfNeeded(); + return signedState == SIGNED; + } + finally { + lock.release(); + } + } + + @Override + public int getMinimumPossibleLength() { + lock.acquire(); + try { + if (valueMap.isEmpty()) { + return 1; + } + long minValue = valueMap.firstKey(); + long maxValue = valueMap.lastKey(); + boolean hasNegativeValues = minValue < 0; + + // check the min and max values in this enum to see if they fit in 1 byte enum, then + // 2 byte enum, then 4 byte enum. If the min min and max values fit, then all other values + // will fit as well + for (int size = 1; size < 8; size *= 2) { + long minPossible = getMinPossibleValue(size, hasNegativeValues); + long maxPossible = getMaxPossibleValue(size, hasNegativeValues); + if (minValue >= minPossible && maxValue <= maxPossible) { + return size; + } + } + return 8; + } + finally { + lock.release(); + } + } } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/EnumSignedState.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/EnumSignedState.java new file mode 100644 index 0000000000..d0f9838caa --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/EnumSignedState.java @@ -0,0 +1,30 @@ +/* ### + * 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.program.database.data; + +/** + * Keeps track of the signed state of an enum datatype. Enum are fundamentally either signed or + * unsigned, but sometimes you can't tell based on the values they contain. Once a negative value + * is added, then the enum becomes locked as signed, preventing high unsigned values from being + * added. Once a high value unsigned value is added, then it becomes locked as unsigned value. If + * neither a negative value or high unsigned value has been added, then the enum is not locked as + * either signed or unsigned. + */ +public enum EnumSignedState { + SIGNED, // Enum contains at least 1 negative value, preventing high unsigned values + UNSIGNED, // Enum contains at least 1 high unsigned value, preventing negative values + NONE // Enum contains neither a negative or a high unsigned value, so can go either way +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Enum.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Enum.java index bb58033351..774a99b631 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Enum.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Enum.java @@ -107,4 +107,50 @@ public interface Enum extends DataType { * @return formatted integer string */ public String getRepresentation(BigInteger bigInt, Settings settings, int bitLength); + + /** + * Returns true if this enum has an entry with the given name. + * @param name the name to check for an entry + * @return true if this enum has an entry with the given name + */ + public boolean contains(String name); + + /** + * Returns true if this enum has an entry with the given value. + * @param value the value to check for an entry + * @return true if this enum has an entry with the given value + */ + public boolean contains(long value); + + /** + * Returns true if the enum contains at least one negative value. Internally, enums have + * three states, signed, unsigned, and none (can't tell from the values). If any of + * the values are negative, the enum is considered signed. If any of the values are large + * unsigned values (upper bit set), then it is considered unsigned. This method will return + * true if the enum is signed, and false if it is either unsigned or none (meaning that it + * doesn't matter for the values that are contained in the enum. + * @return true if the enum contains at least one negative value + */ + public boolean isSigned(); + + /** + * Returns the maximum value that this enum can represent based on its size and signedness. + * @return the maximum value that this enum can represent based on its size and signedness. + */ + public long getMaxPossibleValue(); + + /** + * Returns the maximum value that this enum can represent based on its size and signedness. + * @return the maximum value that this enum can represent based on its size and signedness. + */ + public long getMinPossibleValue(); + + /** + * Returns the smallest length (size in bytes) this enum can be and still represent all of + * it's current values. Note that that this will only return powers of 2 (1,2,4, or 8) + * @return the smallest length (size in bytes) this enum can be and still represent all of + * it's current values + */ + public int getMinimumPossibleLength(); + } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/EnumDataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/EnumDataType.java index c2334e4d6c..bb883e7b52 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/EnumDataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/EnumDataType.java @@ -15,6 +15,8 @@ */ package ghidra.program.model.data; +import static ghidra.program.database.data.EnumSignedState.*; + import java.math.BigInteger; import java.util.*; @@ -23,6 +25,7 @@ import org.apache.commons.lang3.StringUtils; import ghidra.docking.settings.Settings; import ghidra.docking.settings.SettingsDefinition; import ghidra.program.database.data.DataTypeUtilities; +import ghidra.program.database.data.EnumSignedState; import ghidra.program.model.mem.MemBuffer; import ghidra.program.model.mem.MemoryAccessException; import ghidra.program.model.scalar.Scalar; @@ -34,11 +37,12 @@ public class EnumDataType extends GenericDataType implements Enum { new SettingsDefinition[] { MutabilitySettingsDefinition.DEF }; private Map nameMap; // name to value - private TreeMap> valueMap; // value to names private Map commentMap; // name to comment + private SortedMap> valueMap; // value to names private int length; private String description; private List bitGroups; + private EnumSignedState signedState = NONE; public EnumDataType(String name, int length) { this(CategoryPath.ROOT, name, length, null); @@ -157,9 +161,27 @@ public class EnumDataType extends GenericDataType implements Enum { if (!StringUtils.isBlank(comment)) { commentMap.put(valueName, comment); } + signedState = computeSignedness(); } + private EnumSignedState computeSignedness() { + if (valueMap.isEmpty()) { + return NONE; + } + long minValue = valueMap.firstKey(); + long maxValue = valueMap.lastKey(); + + if (minValue < 0) { + return SIGNED; + } + if (maxValue > getMaxPossibleValue(length, true)) { + return UNSIGNED; + } + + return NONE; // we have no negatives and no large unsigned values + } + @Override public void remove(String valueName) { bitGroups = null; @@ -182,6 +204,7 @@ public class EnumDataType extends GenericDataType implements Enum { } commentMap.remove(valueName); + signedState = computeSignedness(); } @Override @@ -220,50 +243,90 @@ public class EnumDataType extends GenericDataType implements Enum { if (newLength == length) { return; } - if (newLength < 1 || newLength > 8) { - throw new IllegalArgumentException("Enum length must be between 1 and 8 inclusive"); - } - checkValues(newLength); + int minLength = getMinimumPossibleLength(); + if (newLength < minLength || newLength > 8) { + throw new IllegalArgumentException( + "Enum length must be between " + minLength + "and 8 inclusive"); + + } this.length = newLength; } - private void checkValues(int newLength) { - if (newLength == 8) { - return; // all long values permitted - } - - long newMaxValue = getMaxEnumValue(newLength); - String[] names = getNames(); - for (String valueName : names) { - long value = getValue(valueName); - if (value > newMaxValue) { - throw new IllegalArgumentException("Setting the length of this Enum to a size " + - "that cannot contain the current value for \"" + valueName + "\" of 0x" + - Long.toHexString(value) + "\nOld length: " + length + "; new length: " + - newLength); - } - } - } - private void checkValue(long value) { if (length == 8) { return; // all long values permitted } - long max = getMaxEnumValue(length); - if (value > max) { + long min = getMinPossibleValue(); + long max = getMaxPossibleValue(); + if (value < min || value > max) { throw new IllegalArgumentException( - getName() + " enum value 0x" + Long.toHexString(value) + - " is outside the range of 0x0 to 0x" + Long.toHexString(max)); - + "Attempted to add a value outside the range for this enum: (" + min + ", " + max + + "): " + value); } } - private long getMaxEnumValue(int bytes) { - int bits = bytes * 8; // number of bits used for the given size - long power2 = 1L << bits; // 2^length is the number of values that 'bytes' can represent - return power2 - 1; // max value is always 1 less than 2^length (0-based) + @Override + public boolean isSigned() { + return signedState == SIGNED; + } + + @Override + public long getMinPossibleValue() { + return getMinPossibleValue(length, signedState != UNSIGNED); + } + + @Override + public long getMaxPossibleValue() { + return getMaxPossibleValue(length, signedState == SIGNED); + } + + @Override + public int getMinimumPossibleLength() { + if (valueMap.isEmpty()) { + return 1; + } + + long minValue = valueMap.firstKey(); + long maxValue = valueMap.lastKey(); + boolean hasNegativeValues = minValue < 0; + + // check the min and max values in this enum to see if they fit in 1 byte enum, then + // 2 byte enum, then 4 byte enum. If the min min and max values fit, then all other values + // will fit as well + for (int size = 1; size < 8; size *= 2) { + long minPossible = getMinPossibleValue(size, hasNegativeValues); + long maxPossible = getMaxPossibleValue(size, hasNegativeValues); + if (minValue >= minPossible && maxValue <= maxPossible) { + return size; + } + } + return 8; + } + + private long getMaxPossibleValue(int bytes, boolean allowNegativeValues) { + if (bytes == 8) { + return Long.MAX_VALUE; + } + int bits = bytes * 8; + if (allowNegativeValues) { + bits -= 1; // take away 1 bit for the sign + } + + // the largest value that can be held in n bits in 2^n -1 + return (1L << bits) - 1; + } + + private long getMinPossibleValue(int bytes, boolean allowNegativeValues) { + if (!allowNegativeValues) { + return 0; + } + int bits = bytes * 8; + + // smallest value (largest negative) that can be stored in n bits is when the sign bit + // is on (and sign extended), and all less significant bits are 0 + return -1L << (bits - 1); } @Override @@ -438,10 +501,30 @@ public class EnumDataType extends GenericDataType implements Enum { for (String valueName : names) { add(valueName, enumm.getValue(valueName), enumm.getComment(valueName)); } + computeSignedness(); } @Override public String getDefaultLabelPrefix() { return name; } + + @Override + public boolean contains(String entryName) { + return nameMap.containsKey(entryName); + + } + + @Override + public boolean contains(long value) { + return valueMap.containsKey(value); + } + + /** + * Sets this enum to it smallest (power of 2) size that it can be and still represent all its + * current values. + */ + public void pack() { + setLength(getMinimumPossibleLength()); + } } diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/data/EnumDataTypeTest.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/data/EnumDataTypeTest.java index d68b105bf2..fb91f762c9 100644 --- a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/data/EnumDataTypeTest.java +++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/data/EnumDataTypeTest.java @@ -15,6 +15,8 @@ */ package ghidra.program.model.data; +import static org.junit.Assert.*; + import org.junit.*; import ghidra.program.model.address.Address; @@ -52,6 +54,244 @@ public class EnumDataTypeTest { BigEndianDataConverter.INSTANCE.getBytes(Integer.MIN_VALUE), true); Assert.assertEquals("bob", enumDt.getRepresentation(memBuffer, null, 0)); + } + + @Test + public void testCanAddHighUnsignedValue() { + EnumDataType enumDt = new EnumDataType("Test", 1); + enumDt.add("a", 0xff); + assertEquals(0xff, enumDt.getValue("a")); + } + + @Test + public void testCanAddNegativeValue() { + EnumDataType enumDt = new EnumDataType("Test", 1); + enumDt.add("a", -1); + assertEquals(-1, enumDt.getValue("a")); + } + + @Test + public void testCantAddNegativeAndHighUnsignedValue() { + EnumDataType enumDt = new EnumDataType("Test", 1); + enumDt.add("a", -1); + try { + enumDt.add("b", 0xff); + fail("Expected Illegal ArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testCantAddHighUnsignedAndNegativeValue() { + EnumDataType enumDt = new EnumDataType("Test", 1); + enumDt.add("a", 0xff); + try { + enumDt.add("b", -1); + fail("Expected Illegal ArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testCanAddNegativeAfterAddingThenRemovingHighUnsignedValue() { + EnumDataType enumDt = new EnumDataType("Test", 1); + enumDt.add("a", 0xff); + enumDt.remove("a"); + enumDt.add("a", -1); + assertEquals(-1, enumDt.getValue("a")); + } + + @Test + public void testGetMinPossibleWidthUnsigned() { + EnumDataType enumDt = new EnumDataType("Test", 4); + assertEquals(1, enumDt.getMinimumPossibleLength()); + + enumDt.add("a", 0); + assertEquals(1, enumDt.getMinimumPossibleLength()); + + enumDt.add("b", 0xff); + assertEquals(1, enumDt.getMinimumPossibleLength()); + + enumDt.add("c", 0x100); + assertEquals(2, enumDt.getMinimumPossibleLength()); + + enumDt.add("d", 0xffff); + assertEquals(2, enumDt.getMinimumPossibleLength()); + + enumDt.add("e", 0x1ffff); + assertEquals(4, enumDt.getMinimumPossibleLength()); + + enumDt.add("f", 0xffffffffL); + assertEquals(4, enumDt.getMinimumPossibleLength()); } + + @Test + public void testGetMinPossibleLengthFor8ByteEnum() { + EnumDataType enumDt = new EnumDataType("Test", 8); + enumDt.add("a", -1); + assertEquals(1, enumDt.getMinimumPossibleLength()); + + } + + @Test + public void test8ByteEnums() { + EnumDataType enumDt = new EnumDataType("Test", 8); + enumDt.add("a", 0); + assertEquals(Long.MIN_VALUE, enumDt.getMinPossibleValue()); + assertEquals(Long.MAX_VALUE, enumDt.getMaxPossibleValue()); + assertFalse(enumDt.isSigned()); + enumDt.add("b", -1); + assertTrue(enumDt.isSigned()); + } + + @Test + public void testGetMinPossibleWidthSigned() { + EnumDataType enumDt = new EnumDataType("Test", 4); + assertEquals(1, enumDt.getMinimumPossibleLength()); + + enumDt.add("a", 0); + assertEquals(1, enumDt.getMinimumPossibleLength()); + + enumDt.add("b", -0x1); + assertEquals(1, enumDt.getMinimumPossibleLength()); + + enumDt.add("c", 0xff); + assertEquals(2, enumDt.getMinimumPossibleLength()); + + enumDt.add("d", 0xffff); + assertEquals(4, enumDt.getMinimumPossibleLength()); + + enumDt.add("e", 0x1ffff); + assertEquals(4, enumDt.getMinimumPossibleLength()); + + } + + @Test + public void testGetMinPossibleWidthSignedWithBigNegatives() { + EnumDataType enumDt = new EnumDataType("Test", 4); + assertEquals(1, enumDt.getMinimumPossibleLength()); + + enumDt.add("a", 0); + assertEquals(1, enumDt.getMinimumPossibleLength()); + + enumDt.add("b", -0x1); + assertEquals(1, enumDt.getMinimumPossibleLength()); + + enumDt.add("c", -0xff); + assertEquals(2, enumDt.getMinimumPossibleLength()); + + enumDt.add("d", -0xffff); + assertEquals(4, enumDt.getMinimumPossibleLength()); + + enumDt.add("e", -0x1ffff); + assertEquals(4, enumDt.getMinimumPossibleLength()); + + } + + @Test + public void testMinMaxPossibleValuesNoSignednessSize1() { + EnumDataType enumDt = new EnumDataType("Test", 1); + assertEquals(-0x80, enumDt.getMinPossibleValue()); + assertEquals(0xff, enumDt.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesNoSignednessSize2() { + EnumDataType enumDt = new EnumDataType("Test", 2); + assertEquals(-0x8000, enumDt.getMinPossibleValue()); + assertEquals(0xffff, enumDt.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesNoSignednessSize4() { + EnumDataType enumDt = new EnumDataType("Test", 4); + assertEquals(-0x80000000L, enumDt.getMinPossibleValue()); + assertEquals(0xffffffffL, enumDt.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesNoSignednessSize8() { + EnumDataType enumDt = new EnumDataType("Test", 8); + assertEquals(Long.MIN_VALUE, enumDt.getMinPossibleValue()); + assertEquals(Long.MAX_VALUE, enumDt.getMaxPossibleValue()); + } + + public void testMinMaxPossibleValuesSignedSize1() { + EnumDataType enumDt = new EnumDataType("Test", 1); + enumDt.add("a", -1); // this makes it an signed enum + assertEquals(-0x80, enumDt.getMinPossibleValue()); + assertEquals(0x7f, enumDt.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesSignedSize2() { + EnumDataType enumDt = new EnumDataType("Test", 2); + enumDt.add("a", -1); // this makes it an signed enum + assertEquals(-0x8000, enumDt.getMinPossibleValue()); + assertEquals(0x7fff, enumDt.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesSignedSize4() { + EnumDataType enumDt = new EnumDataType("Test", 4); + enumDt.add("a", -1); // this makes it an signed enum + assertEquals(-0x80000000L, enumDt.getMinPossibleValue()); + assertEquals(0x7fffffffL, enumDt.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesSignedSize8() { + EnumDataType enumDt = new EnumDataType("Test", 8); + enumDt.add("a", -1); // this makes it an signed enum + assertEquals(Long.MIN_VALUE, enumDt.getMinPossibleValue()); + assertEquals(Long.MAX_VALUE, enumDt.getMaxPossibleValue()); + } + + public void testMinMaxPossibleValuesUnsignedSize1() { + EnumDataType enumDt = new EnumDataType("Test", 1); + enumDt.add("a", 0xff); // this makes it an unsigned enum + assertEquals(0, enumDt.getMinPossibleValue()); + assertEquals(0xff, enumDt.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesUnsignedSize2() { + EnumDataType enumDt = new EnumDataType("Test", 2); + enumDt.add("a", 0xffff); // this makes it an signed enum + assertEquals(0, enumDt.getMinPossibleValue()); + assertEquals(0xffff, enumDt.getMaxPossibleValue()); + } + + @Test + public void testMinMaxPossibleValuesUnsignedSize4() { + EnumDataType enumDt = new EnumDataType("Test", 4); + enumDt.add("a", 0xffffffffL); // this makes it an signed enum + assertEquals(0, enumDt.getMinPossibleValue()); + assertEquals(0xffffffffL, enumDt.getMaxPossibleValue()); + } + + @Test + public void testContainsName() { + EnumDataType enumDt = new EnumDataType("Test", 4); + enumDt.add("a", 0x1); + enumDt.add("b", -1); + assertTrue(enumDt.contains("a")); + assertTrue(enumDt.contains("b")); + assertFalse(enumDt.contains("c")); + } + + @Test + public void testContainsValue() { + EnumDataType enumDt = new EnumDataType("Test", 4); + enumDt.add("a", 0x1); + enumDt.add("b", -1); + assertTrue(enumDt.contains(-1)); + assertTrue(enumDt.contains(1)); + assertFalse(enumDt.contains(3)); + } }