From 5b3ff029836c91dde528445c7e15de825de5e3c4 Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:10:06 -0400 Subject: [PATCH 1/2] GP-4952: Iterate in the correct direction. --- .../src/main/java/ghidra/pcode/exec/ValueLocation.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/ValueLocation.java b/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/ValueLocation.java index caa24b7883..6ba28fc57c 100644 --- a/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/ValueLocation.java +++ b/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/exec/ValueLocation.java @@ -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. @@ -170,7 +170,7 @@ public class ValueLocation { ListIterator itB = that.nodes.listIterator(that.nodeCount()); Varnode[] result = new Varnode[Math.max(this.nodeCount(), that.nodeCount())]; int i = result.length; - while (itA.hasNext() && itB.hasPrevious()) { + while (itA.hasPrevious() && itB.hasPrevious()) { Varnode vnA = itA.previous(); Varnode vnB = itB.previous(); if (vnA.getSize() != vnB.getSize()) { From ef724708df27c042509d8324d1a2a2f5c568bd04 Mon Sep 17 00:00:00 2001 From: ghidra1 Date: Wed, 25 Sep 2024 13:41:13 -0400 Subject: [PATCH 2/2] GP-4949 Added Structure.setLength method and made structure editor performance improvements and various bug fixes. --- .../core/compositeeditor/CompEditorModel.java | 186 ++++++++---------- .../CompositeEditorTableAction.java | 44 +---- .../compositeeditor/CompositeViewerModel.java | 1 + .../compositeeditor/StructureEditorModel.java | 143 ++++++-------- .../compositeeditor/UnionEditorModel.java | 11 -- .../core/stackeditor/BiDirectionDataType.java | 9 +- .../core/stackeditor/StackEditorPanel.java | 2 +- .../StructureEditorAlignmentTest.java | 6 +- .../StructureEditorLockedActions3Test.java | 6 +- .../StructureEditorLockedDnDTest.java | 6 +- .../StructureEditorUnlockedActions2Test.java | 10 +- .../StructureEditorUnlockedActions5Test.java | 2 +- .../StructureEditorUnlockedDnD4Test.java | 8 +- .../model/data/StructureDataTypeTest.java | 39 +++- .../program/database/data/StructureDB.java | 50 ++++- .../model/data/StandAloneDataTypeManager.java | 1 + .../ghidra/program/model/data/Structure.java | 17 +- .../program/model/data/StructureDataType.java | 36 +++- .../database/data/StructureDBTest.java | 34 ++++ 19 files changed, 341 insertions(+), 270 deletions(-) diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompEditorModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompEditorModel.java index 969ed028c0..a158ae3f6a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompEditorModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompEditorModel.java @@ -17,6 +17,8 @@ package ghidra.app.plugin.core.compositeeditor; import java.util.*; +import javax.help.UnsupportedOperationException; + import docking.widgets.OptionDialog; import docking.widgets.fieldpanel.support.*; import ghidra.program.database.DatabaseObject; @@ -586,7 +588,7 @@ public abstract class CompEditorModel extends CompositeEditorModel { @Override public DataTypeComponent add(int rowIndex, DataType dt) throws UsrException { String descr = rowIndex < getNumComponents() ? "Replace Component" : "Add Component"; - return viewDTM.withTransaction(descr, () -> { + DataTypeComponent dtc = viewDTM.withTransaction(descr, () -> { DataType resolvedDt = viewDTM.resolve(dt, DataTypeConflictHandler.DEFAULT_HANDLER); try { DataTypeInstance dti = getDropDataType(rowIndex, resolvedDt); @@ -596,7 +598,11 @@ public abstract class CompEditorModel extends CompositeEditorModel { return null; } }); - + + fixSelection(); + componentEdited(); + selectionChanged(); + return dtc; } /** @@ -628,6 +634,10 @@ public abstract class CompEditorModel extends CompositeEditorModel { else { dtc = viewDTM.withTransaction("Add Component", () -> insert(rowIndex, dt, dtLength)); } + + fixSelection(); + componentEdited(); + selectionChanged(); return dtc; } @@ -733,10 +743,8 @@ public abstract class CompEditorModel extends CompositeEditorModel { int sizeDiff = newCompSize - oldCompSize; // New one is larger so check to make sure it will fit. - if (sizeDiff > 0) { - if (!checkForReplace(rowIndex, datatype)) { - throw new InvalidDataTypeException(datatype.getDisplayName() + " doesn't fit."); - } + if (!isAtEnd(rowIndex) && sizeDiff > 0) { + checkForReplace(rowIndex, datatype, newCompSize); } // Replace the component at index. @@ -811,35 +819,76 @@ public abstract class CompEditorModel extends CompositeEditorModel { * * @param rowIndex index of the row (component). * @param datatype the type - * @return true if the replace is allowed + * @param length component length + * @throws InvalidDataTypeException if check fails */ - boolean checkForReplace(int rowIndex, DataType datatype) { + private void checkForReplace(int rowIndex, DataType datatype, int length) throws InvalidDataTypeException { DataTypeComponent dtc = getComponent(rowIndex); if (dtc == null) { - return false; + throw new InvalidDataTypeException("Invalid component selection"); } - if (!isShowingUndefinedBytes()) { - return true; + if (!(viewComposite instanceof Structure struct)) { + return; } + if (struct.isPackingEnabled()) { + return; + } + if (isAtEnd(rowIndex)) { + return; + } + // Does the new data type fit by replacing the component at index. // Get the current data type at the index. - DataTypeComponent comp = getComponent(rowIndex); - int currentCompSize = comp.getLength(); - int newCompSize = datatype.getLength(); + int currentCompSize = dtc.getLength(); + int newCompSize = length; int sizeDiff = newCompSize - currentCompSize; - int numUndefs = 0; - - // New one is larger. - if (sizeDiff > 0) { - if (isAtEnd(rowIndex) || onlyUndefinedsUntilEnd(rowIndex + 1)) { - return true; - } - // structure needs to have enough undefined bytes or replace fails. - numUndefs = getNumUndefinedBytesAt(rowIndex + 1); + + if (sizeDiff <= 0) { + return; } - - return (sizeDiff <= numUndefs); + + int undefinedSpaceAvail = getNumUndefinedBytesAfter(dtc); + if (sizeDiff > undefinedSpaceAvail) { + int spaceNeeded = sizeDiff - undefinedSpaceAvail; + String msg = newCompSize + " byte replacement at 0x" + Integer.toHexString(dtc.getOffset()); + if (struct.getDefinedComponentAtOrAfterOffset(dtc.getOffset() + 1) == null) { + // suggest growing structure + int suggestedSize = getLength() + spaceNeeded; + throw new InvalidDataTypeException(msg + " requires structure length of " + suggestedSize + "-bytes."); + } + // suggest insert bytes (NOTE: in the future a conflict removal/grow could be offered) + throw new InvalidDataTypeException(msg + " requires " + spaceNeeded + " additional undefined bytes."); + } + } + + /** + * Get the number of undefined bytes after the specified component. + * The viewComposite must be a non-packed structure. + * @param dtc datatype component + * @return number of undefined bytes after non-packed structure component or -1 if no additional + * defined components exist which will impead component growth or placement. + */ + protected final int getNumUndefinedBytesAfter(DataTypeComponent dtc) { + if (!isShowingUndefinedBytes()) { + throw new UnsupportedOperationException(); + } + if (!(viewComposite instanceof Structure struct)) { + throw new UnsupportedOperationException(); + } + if (struct.isPackingEnabled()) { + throw new UnsupportedOperationException(); + } + + // TODO: May need special logic if dtc is zero-length component + int length = getLength(); + int nextCompOffset = dtc.getEndOffset() + 1; + if (nextCompOffset >= length) { + return 0; + } + DataTypeComponent nextDefinedDtc = struct.getDefinedComponentAtOrAfterOffset(nextCompOffset); + int nextDefinedOffset = (nextDefinedDtc == null) ? length : nextDefinedDtc.getOffset(); + return Math.max(0, nextDefinedOffset - nextCompOffset); // prevent negative return value } /** @@ -1013,34 +1062,6 @@ public abstract class CompEditorModel extends CompositeEditorModel { }); } - /** - * Returns the number of undefined bytes that are available in the structure - * beginning at the specified row index. - * - * @param rowIndex the index of the row - * @return the number of bytes - */ - protected int getNumUndefinedBytesAt(int rowIndex) { - int numRowComponents = getNumComponents(); - if (rowIndex < 0 || rowIndex >= numRowComponents) { - return 0; - } - DataTypeComponent startComponent = getComponent(rowIndex); - int previousOffset = (startComponent != null) ? startComponent.getOffset() : 0; - for (int currentRowIndex = - rowIndex; currentRowIndex < numRowComponents; currentRowIndex++) { - // Get the current data type at the index. - DataTypeComponent comp = getComponent(currentRowIndex); - DataType dt = comp.getDataType(); - int currentOffset = comp.getOffset(); - if (!dt.equals(DataType.DEFAULT)) { - return currentOffset - previousOffset; // Ran into data type other than undefined byte. - } - } - - return viewComposite.getLength() - previousOffset; - } - /** * Determine if the indicated row index is at or beyond the last component in this composite. * @@ -1062,36 +1083,6 @@ public abstract class CompEditorModel extends CompositeEditorModel { return false; } - /** - * Determine whether or not there are only undefined data types from the indicated rowIndex - * until the end of the composite. There must be at least one undefined data type to return true. - * - * @param rowIndex the index of the row to begin checking for undefined data types. - * @return true if an undefined data type is at the indicated row index and all components - * from there to the end of the composite are undefined data types. - */ - protected boolean onlyUndefinedsUntilEnd(int rowIndex) { - if (!isShowingUndefinedBytes()) { - return false; - } - int numRowComponents = getNumComponents(); - if (rowIndex < 0) { - return false; - } - if (rowIndex >= numRowComponents) { - return false; // Beyond last component. - } - for (int i = rowIndex; i < numRowComponents; i++) { - // Get the current data type at the index. - DataTypeComponent comp = getComponent(i); - DataType dt = comp.getDataType(); - if (!dt.equals(DataType.DEFAULT)) { - return false; // Ran into data type other than undefined byte. - } - } - return true; - } - /** * Cause the component at the specified index to consume undefined bytes * that follow it. @@ -1670,16 +1661,17 @@ public abstract class CompEditorModel extends CompositeEditorModel { if ((rowIndex < 0) || (rowIndex >= numRowComponents)) { return 0; } - if (rowIndex + 1 == numRowComponents) { - return Integer.MAX_VALUE; // On last component. + DataTypeComponent dtc = getComponent(rowIndex); + DataType dt = dtc.getDataType(); + int dtcLen = dt.getLength(); + if (dtcLen < 0) { + dtcLen = dtc.getLength(); } - DataType dt = getComponent(rowIndex).getDataType(); - int maxDups = Integer.MAX_VALUE; - // If editModel is showing undefined bytes (non-packed) - // then constrain by number of undefined bytes that follow. - if (isShowingUndefinedBytes() && (dt != DataType.DEFAULT)) { - int numBytes = getNumUndefinedBytesAt(rowIndex + 1); - maxDups = (numBytes / dt.getLength()); + int maxDups = (Integer.MAX_VALUE - getLength()) / dtcLen; + if (dt != DataType.DEFAULT && isShowingUndefinedBytes() && !isAtEnd(rowIndex)) { + // If editModel is showing undefined bytes (non-packed) + // then constrain by number of undefined bytes that follow. + maxDups = getNumUndefinedBytesAfter(dtc) / dtcLen; } return maxDups; } @@ -1715,15 +1707,7 @@ public abstract class CompEditorModel extends CompositeEditorModel { return numBytesInRange / len; } // single line selected. - - // If editModel is locked then constrain by number of undefined bytes that follow. - if (!isShowingUndefinedBytes() || isAtEnd(rowIndex) || - onlyUndefinedsUntilEnd(rowIndex + 1)) { - return Integer.MAX_VALUE; - } - - int numBytes = getNumUndefinedBytesAt(rowIndex + 1); - return 1 + (numBytes / len); + return getMaxDuplicates(rowIndex) + 1; } return 0; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorTableAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorTableAction.java index 8088fb2712..29ec596101 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorTableAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorTableAction.java @@ -30,8 +30,7 @@ import ghidra.util.HelpLocation; *

* Note: Any new actions must be registered in the editor manager via the actions's name. */ -abstract public class CompositeEditorTableAction extends DockingAction - implements CompositeEditorModelListener { +abstract public class CompositeEditorTableAction extends DockingAction { static final String MAIN_ACTION_GROUP = "0_MAIN_EDITOR_ACTION"; static final String UNDOREDO_ACTION_GROUP = "1_UNDOREDO_EDITOR_ACTION"; @@ -79,14 +78,12 @@ abstract public class CompositeEditorTableAction extends DockingAction this.model = provider.getModel(); this.plugin = provider.plugin; this.tool = plugin.getTool(); - model.addCompositeEditorModelListener(this); String helpAnchor = provider.getHelpName() + "_" + getHelpName(); setHelpLocation(new HelpLocation(provider.getHelpTopic(), helpAnchor)); } @Override public void dispose() { - model.removeCompositeEditorModelListener(this); super.dispose(); provider = null; model = null; @@ -108,43 +105,4 @@ abstract public class CompositeEditorTableAction extends DockingAction return getName(); } - @Override - public void selectionChanged() { - provider.contextChanged(); - } - - public void editStateChanged(int i) { - provider.contextChanged(); - } - - @Override - public void compositeEditStateChanged(int type) { - provider.contextChanged(); - } - - @Override - public void endFieldEditing() { - provider.contextChanged(); - } - - @Override - public void componentDataChanged() { - provider.contextChanged(); - } - - @Override - public void compositeInfoChanged() { - provider.contextChanged(); - } - - @Override - public void statusChanged(String message, boolean beep) { - // we are an action; don't care about status messages - } - - @Override - public void showUndefinedStateChanged(boolean showUndefinedBytes) { - provider.contextChanged(); - } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeViewerModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeViewerModel.java index c344963b50..009cab7a42 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeViewerModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeViewerModel.java @@ -1146,6 +1146,7 @@ abstract class CompositeViewerModel extends AbstractTableModel for (CompositeViewerModelListener listener : modelListeners) { listener.selectionChanged(); } + provider.contextChanged(); }); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorModel.java index b38ec4a3ba..bb1007ec14 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorModel.java @@ -215,36 +215,8 @@ class StructureEditorModel extends CompEditorModel { } viewDTM.withTransaction("Set Size", () -> { - int length = currentLength; Structure structure = (Structure) viewComposite; - if (length > size) { - int numComponents = structure.getNumComponents(); - - DataTypeComponent dtc = structure.getComponentContaining(size); - int ordinal = dtc.getOrdinal(); - - // retain any zero-length components which have an offset equal the new size - while (dtc.getOffset() == size && dtc.getLength() == 0 && - (ordinal + 1) < numComponents) { - dtc = structure.getComponent(++ordinal); - } - - // remove trailing components outside of new size - for (int index = numComponents - 1; index >= ordinal; index--) { - structure.delete(index); - int bitFieldResidualBytes = structure.getNumComponents() - index; - for (int i = 0; i < bitFieldResidualBytes; i++) { - // bitfield removal may cause injection of undefined bytes - remove them - structure.delete(index); - } - } - // structure may shrink too much from component removal - may need to grow - length = (viewComposite.isZeroLength()) ? 0 : viewComposite.getLength(); - } - if (length < size) { - // Increasing structure length. - structure.growStructure(size - length); - } + structure.setLength(size); }); notifyCompositeChanged(); } @@ -372,13 +344,17 @@ class StructureEditorModel extends CompEditorModel { throw new IllegalArgumentException("Invalid component index specified"); } DataType dt = originalComp.getDataType(); - int dtLen = dt.getLength(); + int len = dt.getLength(); + if (len < 0) { + len = originalComp.getLength(); + } checkIsAllowableDataType(dt); + int dtcLen = len; viewDTM.withTransaction("Duplicate Components", () -> { int startIndex = index + 1; - if (isShowingUndefinedBytes() && (dt != DataType.DEFAULT)) { - int endIndex = startIndex + (dtLen * multiple) - 1; + if (dt != DataType.DEFAULT && isShowingUndefinedBytes() && !isAtEnd(index)) { + int endIndex = startIndex + (dtcLen * multiple) - 1; if (startIndex < getNumComponents()) { deleteComponentRange(startIndex, endIndex, monitor); } @@ -605,21 +581,16 @@ class StructureEditorModel extends CompEditorModel { if (comp == null) { return false; } - DataType dt = comp.getDataType(); - if (viewComposite.isPackingEnabled()) { + if (viewComposite.isPackingEnabled() || isAtEnd(rowIndex)) { return true; } + DataType dt = comp.getDataType(); if (dt.equals(DataType.DEFAULT)) { return true; // Insert an undefined and push everything down. } if (comp.isBitFieldComponent()) { return false; // unable to place non-packed bitfield in a reasonable fashion } - // Can always duplicate at the end. - if (isAtEnd(rowIndex) || onlyUndefinedsUntilEnd(rowIndex + 1)) { - return true; - } - // Otherwise can only duplicate if enough room. // Get the size of the data type at this index and the number of // undefined bytes following it. @@ -627,8 +598,7 @@ class StructureEditorModel extends CompEditorModel { if (dtSize <= 0) { dtSize = comp.getLength(); } - int undefSize = getNumUndefinedBytesAt(rowIndex + 1); - if (dtSize <= undefSize) { + if (dtSize <= getNumUndefinedBytesAfter(comp)) { return true; } return false; @@ -701,8 +671,7 @@ class StructureEditorModel extends CompEditorModel { 1 == currentRange.getEnd().getIndex().intValue()); if (isOneComponent) { - if (!isShowingUndefinedBytes() || isAtEnd(currentIndex) || - onlyUndefinedsUntilEnd(currentIndex + 1)) { + if (isPackingEnabled() || isAtEnd(currentIndex)) { return true; // allow replace of component when aligning. } @@ -711,10 +680,6 @@ class StructureEditorModel extends CompEditorModel { DataTypeComponent comp = getComponent(currentIndex); if (comp != null) { DataType compDt = comp.getDataType(); - int numCompBytes = comp.getLength(); - int numFollowing = getNumUndefinedBytesAt(currentIndex + 1); - int numAvailable = numCompBytes + numFollowing; - // Drop on pointer. if (compDt instanceof Pointer || DataTypeHelper.getBaseType(compDt) instanceof Pointer) { // Don't create undefined byte pointers. @@ -723,10 +688,8 @@ class StructureEditorModel extends CompEditorModel { } return true; } - else if (datatype.getLength() <= numAvailable) { - return true; - } - return false; + int numAvailable = comp.getLength() + getNumUndefinedBytesAfter(comp); + return datatype.getLength() <= numAvailable; } return true; } @@ -774,17 +737,18 @@ class StructureEditorModel extends CompEditorModel { catch (InvalidDataTypeException e) { return false; } - - if (isShowingUndefinedBytes()) { - if (isAtEnd(rowIndex)) { - return true; - } - int maxBytes = dtc.getLength() + getNumUndefinedBytesAt(rowIndex + 1); - if (dataType.getLength() > maxBytes) { - return false; - } + + if (isPackingEnabled() || isAtEnd(rowIndex)) { + return true; } - return true; + + int undefSize = getNumUndefinedBytesAfter(dtc); + if (undefSize < 0) { + return true; + } + + int numAvailable = dtc.getLength() + undefSize; + return dataType.getLength() <= numAvailable; } // ************************************************************* @@ -804,9 +768,11 @@ class StructureEditorModel extends CompEditorModel { */ @Override public int getMaxAddLength(int rowIndex) { - int maxLength = Integer.MAX_VALUE; if (rowIndex >= getNumComponents() - 1) { - return maxLength; + return Integer.MAX_VALUE; + } + if (isPackingEnabled() || isAtEnd(rowIndex)) { + return Integer.MAX_VALUE; } DataTypeComponent comp = getComponent(rowIndex); FieldRange currentRange = getSelectedRangeContaining(rowIndex); @@ -817,18 +783,9 @@ class StructureEditorModel extends CompEditorModel { 1 == currentRange.getEnd().getIndex().intValue()); if (isOneComponent) { - if (!isShowingUndefinedBytes()) { - return maxLength; - } - - // FreeForm editing mode (showing Undefined Bytes). - int numAvailable = comp.getLength() + getNumUndefinedBytesAt(rowIndex + 1); - return (maxLength == -1) ? numAvailable : Math.min(maxLength, numAvailable); + return comp.getLength() + getNumUndefinedBytesAfter(comp); } - DataTypeComponent startComp = getComponent(currentRange.getStart().getIndex().intValue()); - DataTypeComponent endComp = getComponent(currentRange.getEnd().getIndex().intValue() - 1); - int numAvailable = endComp.getOffset() + endComp.getLength() - startComp.getOffset(); - return (maxLength == -1) ? numAvailable : Math.min(maxLength, numAvailable); + return getNumBytesInRange(currentRange); } /** @@ -842,26 +799,34 @@ class StructureEditorModel extends CompEditorModel { */ @Override public int getMaxReplaceLength(int currentIndex) { - if (!isShowingUndefinedBytes()) { // Can replace at any index + + if (currentIndex >= getNumComponents() - 1) { return Integer.MAX_VALUE; } + if (isPackingEnabled() || isAtEnd(currentIndex)) { + return Integer.MAX_VALUE; + } + // Can only replace with what fits unless at last component or empty last line. DataTypeComponent comp = getComponent(currentIndex); int numComponents = getNumComponents(); if ((currentIndex >= (numComponents - 1)) && (currentIndex <= numComponents)) { return Integer.MAX_VALUE; // Last component or empty entry immediately after it. } - else if (comp == null) { + if (comp == null) { return 0; // No such component. Not at valid edit index. } // Otherwise, get size of component and number of Undefined bytes after it. - FieldRange range = getSelectedRangeContaining(currentIndex); - if (range == null || - range.getStart().getIndex().intValue() == range.getEnd().getIndex().intValue() - 1) { - return comp.getLength() + getNumUndefinedBytesAt(currentIndex + 1); + FieldRange currentRange = getSelectedRangeContaining(currentIndex); + boolean isOneComponent = + (currentRange == null) || (currentRange.getStart().getIndex().intValue() + + 1 == currentRange.getEnd().getIndex().intValue()); + + if (isOneComponent) { + return comp.getLength() + getNumUndefinedBytesAfter(comp); } - return getNumBytesInRange(range); + return getNumBytesInRange(currentRange); } /** @@ -959,7 +924,7 @@ class StructureEditorModel extends CompEditorModel { int componentOrdinal = convertRowToOrdinal(rowIndex); // FreeForm editing mode (showing Undefined Bytes). - if (isShowingUndefinedBytes() && !isAtEnd(rowIndex)) { + if (!isPackingEnabled() && !isAtEnd(rowIndex)) { int origLen = getComponent(rowIndex).getLength(); dtc = viewDTM.withTransaction("Replace Component", () -> { return ((Structure) viewComposite).replace(componentOrdinal, dataType, length, @@ -983,14 +948,22 @@ class StructureEditorModel extends CompEditorModel { } else { dtc = viewDTM.withTransaction("Replace Component", () -> { - ((Structure) viewComposite).delete(componentOrdinal); - return ((Structure) viewComposite).insert(componentOrdinal, dataType, length, - name, comment); + Structure struct = (Structure) viewComposite; + DataTypeComponent comp = getComponent(rowIndex); + if (!isPackingEnabled()) { + // We are at end with packing disabled - grow structure if needed + int avail = comp.getLength() + getNumUndefinedBytesAfter(comp); + if (length > avail) { + struct.growStructure(length - avail); + } + } + return ((Structure) viewComposite).replace(componentOrdinal, dataType, length, name, comment); }); } return dtc; } catch (IllegalArgumentException exc) { + // NOTE: Use of exception may cause transaction rollback throw new InvalidDataTypeException(exc.getMessage()); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/UnionEditorModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/UnionEditorModel.java index fa19be5176..2e5f886c08 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/UnionEditorModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/UnionEditorModel.java @@ -525,17 +525,6 @@ class UnionEditorModel extends CompEditorModel { }); } - /** - * Returns the number of undefined bytes that are available in the structure - * beginning at the specified row index. - * - * @param rowIndex the index of the row - */ - @Override - protected int getNumUndefinedBytesAt(int rowIndex) { - return 0; - } - /** * ????? * diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/stackeditor/BiDirectionDataType.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/stackeditor/BiDirectionDataType.java index a3f4af40de..462d812d07 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/stackeditor/BiDirectionDataType.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/stackeditor/BiDirectionDataType.java @@ -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. @@ -393,6 +393,11 @@ public abstract class BiDirectionDataType extends StructureDataType notifySizeChanged(); return dtc; } + + @Override + public void setLength(int len) { + throw new UnsupportedOperationException("setLength not supported"); + } /** * Increases the size of the bidirectional data type If amount is positive then the positive diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/stackeditor/StackEditorPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/stackeditor/StackEditorPanel.java index 06176c2a25..b575d8615f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/stackeditor/StackEditorPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/stackeditor/StackEditorPanel.java @@ -278,7 +278,7 @@ public class StackEditorPanel extends CompositeEditorPanel { @Override public void componentDataChanged() { - // Don't need to update other than table when component data changes. + provider.contextChanged(); } @Override diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorAlignmentTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorAlignmentTest.java index 89ec02305d..40f71402dc 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorAlignmentTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorAlignmentTest.java @@ -593,12 +593,12 @@ public class StructureEditorAlignmentTest extends AbstractStructureEditorTest { assertNotNull(asciiDt); addAtPoint(asciiDt, 2, 3); - assertEquals(3, structureModel.getNumComponents()); - assertEquals(4, structureModel.getRowCount()); + assertEquals(7, structureModel.getNumComponents()); + assertEquals(8, structureModel.getRowCount()); checkRow(0, 0, 1, "db", new ByteDataType(), "", ""); checkRow(1, 1, 4, "float", new FloatDataType(), "", ""); checkRow(2, 5, 1, "char", asciiDt, "", ""); - assertLength(6); + assertLength(10); assertActualAlignment(1); } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorLockedActions3Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorLockedActions3Test.java index bf073be1cf..80f50cd41f 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorLockedActions3Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorLockedActions3Test.java @@ -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. @@ -196,7 +196,7 @@ public class StructureEditorLockedActions3Test extends AbstractStructureEditorTe assertEquals(9, getModel().getNumComponents()); assertEquals("byte", getDataType(1).getName()); assertEquals(getDataType(1), dt1); - assertEquals("pointer doesn't fit.", getModel().getStatus()); + assertTrue(getModel().getStatus().contains("requires 1 additional")); assertEquals(1, getDataType(1).getLength()); assertEquals(1, getModel().getComponent(1).getLength()); assertEquals(DataType.DEFAULT, getDataType(2)); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorLockedDnDTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorLockedDnDTest.java index 9395a448cc..1d2d2ac68e 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorLockedDnDTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorLockedDnDTest.java @@ -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. @@ -78,7 +78,7 @@ public class StructureEditorLockedDnDTest extends AbstractStructureEditorTest { assertTrue(getDataType(1).isEquivalent(dt1)); assertEquals(dt1.getLength(), model.getComponent(1).getLength()); assertEquals(325, model.getLength()); - assertEquals("double doesn't fit.", model.getStatus()); + assertTrue(model.getStatus().contains("requires 7 additional")); } @Test diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorUnlockedActions2Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorUnlockedActions2Test.java index 8e69ee55e2..250241a05d 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorUnlockedActions2Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorUnlockedActions2Test.java @@ -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. @@ -119,14 +119,14 @@ public class StructureEditorUnlockedActions2Test checkSelection(new int[] { 0 }); invoke(action); - assertEquals(1, model.getNumComponents()); + assertEquals(8, model.getNumComponents()); assertTrue(getDataType(0).isEquivalent(new ByteDataType())); assertEquals(1, getLength(0)); checkSelection(new int[] { 0 }); CycleGroupAction floatAction = getCycleGroup(new FloatDataType()); invoke(floatAction); - assertEquals(1, model.getNumComponents()); + assertEquals(5, model.getNumComponents()); assertTrue(getDataType(0).isEquivalent(new FloatDataType())); assertEquals(4, getLength(0)); checkSelection(new int[] { 0 }); @@ -138,7 +138,7 @@ public class StructureEditorUnlockedActions2Test checkSelection(new int[] { 0 }); invoke(floatAction); - assertEquals(1, model.getNumComponents()); + assertEquals(5, model.getNumComponents()); assertTrue(getDataType(0).isEquivalent(new FloatDataType())); assertEquals(4, getLength(0)); checkSelection(new int[] { 0 }); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorUnlockedActions5Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorUnlockedActions5Test.java index 037af8ee59..744406e0e0 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorUnlockedActions5Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorUnlockedActions5Test.java @@ -412,7 +412,7 @@ public class StructureEditorUnlockedActions5Test extends AbstractStructureEditor assertTrue(getDataType(2).isEquivalent(new WordDataType())); invoke(pointerAction); - assertEquals("pointer doesn't fit.", model.getStatus()); + assertTrue(model.getStatus().contains("requires 2 additional")); assertEquals(num, model.getNumComponents()); assertEquals("word", getDataType(2).getDisplayName()); assertTrue(getDataType(2).isEquivalent(new WordDataType())); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorUnlockedDnD4Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorUnlockedDnD4Test.java index 197e3de763..b0eebae125 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorUnlockedDnD4Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/compositeeditor/StructureEditorUnlockedDnD4Test.java @@ -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. @@ -45,7 +45,7 @@ public class StructureEditorUnlockedDnD4Test extends AbstractStructureEditorTest addAtPoint(dt, 0, 3); assertEquals(num, model.getNumComponents()); assertTrue(getDataType(0).isEquivalent(DataType.DEFAULT)); - assertEquals("word doesn't fit.", model.getStatus()); + assertTrue(model.getStatus().contains("requires 1 additional")); dt = programDTM.getDataType("/char"); assertNotNull(dt); @@ -114,7 +114,7 @@ public class StructureEditorUnlockedDnD4Test extends AbstractStructureEditorTest addAtPoint(dt, 5, 3); - assertEquals("qword doesn't fit.", model.getStatus()); + assertTrue(model.getStatus().contains("requires 4 additional")); assertEquals(num, model.getNumComponents()); assertEquals("float", getDataType(5).getDisplayName()); assertEquals(29, model.getLength()); diff --git a/Ghidra/Features/Base/src/test/java/ghidra/program/model/data/StructureDataTypeTest.java b/Ghidra/Features/Base/src/test/java/ghidra/program/model/data/StructureDataTypeTest.java index 69b9c1ea33..4273fe014b 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/program/model/data/StructureDataTypeTest.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/program/model/data/StructureDataTypeTest.java @@ -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. @@ -1046,6 +1046,41 @@ public class StructureDataTypeTest extends AbstractGenericTest { assertEquals(10, comps[3].getOffset()); assertEquals(7, comps[3].getOrdinal()); } + + @Test + public void testSetLength() { + + assertEquals(8, struct.getLength()); + assertEquals(4, struct.getNumComponents()); + assertEquals(4, struct.getNumDefinedComponents()); + + struct.setLength(20); + assertEquals(20, struct.getLength()); + assertEquals(16, struct.getNumComponents()); + assertEquals(4, struct.getNumDefinedComponents()); + + // new length is offcut within 3rd component at offset 0x3 which should get cleared + struct.setLength(4); + assertEquals(4, struct.getLength()); + assertEquals(3, struct.getNumComponents()); + assertEquals(2, struct.getNumDefinedComponents()); + + // Maximum length supported by GUI editor is ~Integer.MAX_VALUE/10 + int len = Integer.MAX_VALUE / 10; + struct.setLength(len); + assertEquals(len, struct.getLength()); + assertEquals(len - 1, struct.getNumComponents()); + assertEquals(2, struct.getNumDefinedComponents()); + + len /= 2; + struct.replaceAtOffset(len-2, WordDataType.dataType, -1, "x", null); // will be preserved below + struct.replaceAtOffset(len+2, WordDataType.dataType, -1, "y", null); // will be cleared below + struct.setLength(len); + assertEquals(len, struct.getLength()); + assertEquals(len - 2, struct.getNumComponents()); + assertEquals(3, struct.getNumDefinedComponents()); + + } @Test public void testDeleteMany() { diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/StructureDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/StructureDB.java index 2ef424b4ca..b74da8a0b9 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/StructureDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/StructureDB.java @@ -236,9 +236,57 @@ class StructureDB extends CompositeDB implements StructureInternal { } } + @Override + public void setLength(int len) { + if (len < 0) { + throw new IllegalArgumentException("Invalid length: " + len); + } + if (len == structLength || isPackingEnabled()) { + return; + } + lock.acquire(); + try { + checkDeleted(); + if (len < structLength) { + // identify index of first defined-component to be removed + int index = Collections.binarySearch(components, Integer.valueOf(len), + OffsetComparator.INSTANCE); + + if (index < 0) { + index = -index - 1; + } + else { + index = backupToFirstComponentContainingOffset(index, len); + } + int definedComponentCount = components.size(); + if (index >= 0 && index < definedComponentCount) { + for (int i = index; i < definedComponentCount; i++) { + doDelete(components.get(i)); + } + components = components.subList(0, index); + } + } + else { + numComponents += len - structLength; + } + structLength = len; + repack(false, false); + notifySizeChanged(false); + } + catch (IOException e) { + dataMgr.dbError(e); + } + finally { + lock.release(); + } + } + @Override public void growStructure(int amount) { - if (isPackingEnabled()) { + if (amount < 0) { + throw new IllegalArgumentException("Invalid growth amount: " + amount); + } + if (amount == 0 || isPackingEnabled()) { return; } lock.acquire(); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StandAloneDataTypeManager.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StandAloneDataTypeManager.java index 2cffd19e08..2dcc1c8981 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StandAloneDataTypeManager.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StandAloneDataTypeManager.java @@ -914,6 +914,7 @@ public class StandAloneDataTypeManager extends DataTypeManagerDB implements Clos } } if (restored) { + invalidateCache(); notifyRestored(); } } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Structure.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Structure.java index e9889f9fb2..8e363bb0d4 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Structure.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Structure.java @@ -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. @@ -430,13 +430,22 @@ public interface Structure extends Composite { String comment) throws IllegalArgumentException; /** - * Increases the size of the structure by the specified amount by adding undefined filler at the + * Increases the size of the structure by the specified positive amount by adding undefined filler at the * end of the structure. NOTE: This method only has an affect on non-packed structures. * * @param amount the amount by which to grow the structure. - * @throws IllegalArgumentException if amount < 1 + * @throws IllegalArgumentException if amount < 0 */ public void growStructure(int amount); + + /** + * Set the size of the structure to the specified byte-length. If the length is shortened defined + * components will be cleared and removed as required. + * NOTE: This method only has an affect on non-packed structures. + * @param length new structure length + * @throws IllegalArgumentException if length < 0 + */ + public void setLength(int length); /** * BitOffsetComparator provides ability to compare an normalized bit offset (see diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StructureDataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StructureDataType.java index 497ea46d18..144b878243 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StructureDataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StructureDataType.java @@ -624,9 +624,43 @@ public class StructureDataType extends CompositeDataTypeImpl implements Structur return dtc; } + @Override + public void setLength(int len) { + if (len < 0) { + throw new IllegalArgumentException("Invalid length: " + len); + } + if (len == structLength || isPackingEnabled()) { + return; + } + if (len < structLength) { + // identify index of first defined-component to be removed + int index = Collections.binarySearch(components, Integer.valueOf(len), + OffsetComparator.INSTANCE); + if (index < 0) { + index = -index - 1; + } + else { + index = backupToFirstComponentContainingOffset(index, len); + } + int definedComponentCount = components.size(); + if (index >= 0 && index < definedComponentCount) { + components = components.subList(0, index); + } + } + else { + numComponents += len - structLength; + } + structLength = len; + repack(false); + notifySizeChanged(); + } + @Override public void growStructure(int amount) { - if (isPackingEnabled()) { + if (amount < 0) { + throw new IllegalArgumentException("Invalid growth amount: " + amount); + } + if (amount == 0 || isPackingEnabled()) { return; } doGrowStructure(amount); diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/StructureDBTest.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/StructureDBTest.java index 7c5cdb3248..32102d1de5 100644 --- a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/StructureDBTest.java +++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/StructureDBTest.java @@ -1449,6 +1449,40 @@ public class StructureDBTest extends AbstractGenericTest { assertEquals(dtc1, barStruct.getComponent(6)); } + + @Test + public void testSetLength() { + + assertEquals(8, struct.getLength()); + assertEquals(4, struct.getNumComponents()); + assertEquals(4, struct.getNumDefinedComponents()); + + struct.setLength(20); + assertEquals(20, struct.getLength()); + assertEquals(16, struct.getNumComponents()); + assertEquals(4, struct.getNumDefinedComponents()); + + // new length is offcut within 3rd component at offset 0x3 which should get cleared + struct.setLength(4); + assertEquals(4, struct.getLength()); + assertEquals(3, struct.getNumComponents()); + assertEquals(2, struct.getNumDefinedComponents()); + + // Maximum length supported by GUI editor is ~Integer.MAX_VALUE/10 + int len = Integer.MAX_VALUE / 10; + struct.setLength(len); + assertEquals(len, struct.getLength()); + assertEquals(len - 1, struct.getNumComponents()); + assertEquals(2, struct.getNumDefinedComponents()); + + len /= 2; + struct.replaceAtOffset(len-2, WordDataType.dataType, -1, "x", null); // will be preserved below + struct.replaceAtOffset(len+2, WordDataType.dataType, -1, "y", null); // will be cleared below + struct.setLength(len); + assertEquals(len, struct.getLength()); + assertEquals(len - 2, struct.getNumComponents()); + assertEquals(3, struct.getNumDefinedComponents()); + } @Test public void testDeleteMany() {