From 290695ae09209036725c43eb30204607748b5985 Mon Sep 17 00:00:00 2001 From: ghidragon <106987263+ghidragon@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:54:47 -0400 Subject: [PATCH] Added feature to close function bodies in the listing display. --- .../DBTraceProgramViewPropertyMapManager.java | 15 +- .../Base/data/base.listing.theme.properties | 1 + .../topics/CodeBrowserPlugin/CodeBrowser.htm | 44 ++++- .../core/codebrowser/CodeViewerProvider.java | 101 +++++++++-- .../viewer/field/AbstractOpenCloseField.java | 2 + .../field/CollapsedCodeFieldFactory.java | 107 +++++++++++ .../field/CollapsedCodeFieldMouseHandler.java | 69 ++++++++ .../viewer/field/FunctionOpenCloseField.java | 93 ++++++++++ .../field/FunctionOpenCloseFieldFactory.java | 110 ++++++++++++ .../app/util/viewer/field/ListingColors.java | 1 + .../field/OpenCloseFieldMouseHandler.java | 6 +- .../viewer/field/SeparatorFieldFactory.java | 8 +- .../viewer/field/VariableOpenCloseField.java | 2 - .../app/util/viewer/format/FormatManager.java | 50 ++++-- .../listingpanel/EmptyListingModel.java | 15 ++ .../viewer/listingpanel/ListingModel.java | 47 +++++ .../viewer/listingpanel/ListingPanel.java | 36 +++- .../listingpanel/ProgramBigListingModel.java | 167 ++++++++++++++---- .../multilisting/ListingModelConverter.java | 15 ++ .../multilisting/MultiListingLayoutModel.java | 15 ++ .../options/ListingDisplayOptionsEditor.java | 7 +- .../app/util/viewer/options/OptionsGui.java | 24 ++- .../util/viewer/options/ScreenElement.java | 16 +- .../app/util/viewer/proxy/FunctionProxy.java | 8 +- ...ger.java => InMemoryOpenCloseManager.java} | 2 +- .../util/PersistentOpenCloseManager.java | 161 +++++++++++++++++ .../viewer/util/ProgramOpenCloseManager.java | 67 +++++-- .../util/PersistentOpenCloseManagerTest.java | 134 ++++++++++++++ .../ProgramBigListingModelTest.java | 162 +++++++++++++++++ .../database/properties/PropertyMapDB.java | 15 +- .../model/util/DefaultPropertyMap.java | 19 +- .../program/model/util/PropertyMap.java | 36 ++-- .../program/util/CollapsedCodeLocation.java | 46 +++++ .../util/FunctionOpenCloseLocation.java | 41 +++++ 34 files changed, 1521 insertions(+), 121 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/CollapsedCodeFieldFactory.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/CollapsedCodeFieldMouseHandler.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/FunctionOpenCloseField.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/FunctionOpenCloseFieldFactory.java rename Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/{AddressBasedOpenCloseManager.java => InMemoryOpenCloseManager.java} (98%) create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/PersistentOpenCloseManager.java create mode 100644 Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/util/PersistentOpenCloseManagerTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/app/util/viewer/listingpanel/ProgramBigListingModelTest.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/CollapsedCodeLocation.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/FunctionOpenCloseLocation.java diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewPropertyMapManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewPropertyMapManager.java index edce235784..3c67291caa 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewPropertyMapManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewPropertyMapManager.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. @@ -48,6 +48,15 @@ public class DBTraceProgramViewPropertyMapManager implements PropertyMapManager return map.getAddressSetView(Lifespan.at(program.snap)); } + @Override + public void clear() { + AddressSetView view = getAddressSetView(); + AddressRangeIterator addressRanges = view.getAddressRanges(); + for (AddressRange range : addressRanges) { + removeRange(range.getMinAddress(), range.getMaxAddress()); + } + } + @Override public boolean intersects(Address start, Address end) { return getAddressSetView().intersects(start, end); @@ -62,7 +71,7 @@ public class DBTraceProgramViewPropertyMapManager implements PropertyMapManager public boolean removeRange(Address start, Address end) { return map.clear(Lifespan.at(program.snap), new AddressRangeImpl(start, end)); } - + @Override public boolean remove(Address addr) { return removeRange(addr, addr); diff --git a/Ghidra/Features/Base/data/base.listing.theme.properties b/Ghidra/Features/Base/data/base.listing.theme.properties index a7b318ab27..a4f8a2e929 100644 --- a/Ghidra/Features/Base/data/base.listing.theme.properties +++ b/Ghidra/Features/Base/data/base.listing.theme.properties @@ -98,6 +98,7 @@ color.fg.listing.pcode.userop = color.palette.blue color.fg.listing.stack.depth = color.palette.blue color.fg.listing.taint = color.fg +color.fg.listing.collapsed = color.palette.gray color.bg.listing.comparison.bytes = color.palette.lightgreen color.bg.listing.comparison.mnemonic = color.palette.lightgreen diff --git a/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/CodeBrowser.htm b/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/CodeBrowser.htm index 35306b3db3..9afd94e085 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/CodeBrowser.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/CodeBrowser.htm @@ -654,27 +654,55 @@ of where in the containing structure the popup menu was activated.

- +

Opening/Closing Functions

+
+

Functions can be open or closed in the listing display. When closed, all the instructions + that make up that function are hidden from view in the listing display. Each consecutive + range of instructions in a function are replaced with a "collapsed code" line to indicate + that some code is hidden and which function the hidden code belongs to. On the function + signature line, there is an open/close control (+/-) that can be used to open or close + a function.

+

The open/close state for functions is persisted across sessions. Also, navigating to + an address in the body of a closed function will cause that function to be opened.

+

Actions for Opening/Closing a Function

+
+

In addition to the control widget in the display, there are also several actions + that can be used to control the open/close state of a single function or all functions. +

+ +
+

Opening/Closing Function Variables Display

The display of parameters and local variables for a function can be toggled on or off. Normally, these are displayed just below the function signature, but they can be turned off to conserve screen space. There is an open/close control (+/-) - on the first parameter/variable line that can be used to toggle them on or off.

+ on the first parameter/variable line that can be used to toggle them on or off. Note that + unlike the open/close state for functions, the open/close state for variables is not + persistent across sessions for a program. The default state of whether or not to + display variables is a tool state is persisted for the tool, not individual programs.

Actions for Opening/Closing Function Variables Display

In addition to the control widget in the display, there are also several actions that can be used to control the display of function variables.

diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeViewerProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeViewerProvider.java index 0bce2b4696..618e11c03c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeViewerProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeViewerProvider.java @@ -131,6 +131,7 @@ public class CodeViewerProvider extends NavigatableComponentProviderAdapter private MultiListingLayoutModel multiModel; private ToggleDockingAction toggleVariablesAction; + private ToggleDockingAction toggleFunctionsAction; public CodeViewerProvider(CodeBrowserPluginInterface plugin, FormatManager formatMgr, boolean isConnected) { @@ -481,24 +482,49 @@ public class CodeViewerProvider extends NavigatableComponentProviderAdapter action = new GotoNextFunctionAction(tool, plugin.getName()); tool.addAction(action); + new ActionBuilder("Open All Functions", plugin.getName()) + .popupMenuPath("Function", "Open All Functions") + .popupMenuGroup("Visibility", "1") + .helpLocation(new HelpLocation("CodeBrowserPlugin", "Open_All_Functions")) + .withContext(ProgramLocationActionContext.class) + .onAction(c -> showAllFunctions(true)) + .buildAndInstallLocal(this); + new ActionBuilder("Close All Functions", plugin.getName()) + .popupMenuPath("Function", "Close All Functions") + .popupMenuGroup("Visibility", "2") + .helpLocation(new HelpLocation("CodeBrowserPlugin", "Close_All_Functions")) + .withContext(ProgramLocationActionContext.class) + .onAction(c -> showAllFunctions(false)) + .buildAndInstallLocal(this); + new ActionBuilder("Toggle Open Function", plugin.getName()) + .popupMenuPath("Function", "Open/Close Function") + .popupMenuGroup("Visibility", "3") + .helpLocation(new HelpLocation("CodeBrowserPlugin", "Toggle_Function")) + .keyBinding("SPACE") + .withContext(ProgramLocationActionContext.class) + .validWhen(this::isInCollapsableCodeArea) + .enabledWhen(this::isInCollapsableCodeArea) + .onAction(c -> toggleShowFunction(c)) + .buildAndInstallLocal(this); + toggleVariablesAction = - new ToggleActionBuilder("Show All Function Variables", plugin.getName()) - .popupMenuPath("Function", "Show/Hide All Variables") - .popupMenuGroup("Variables") + new ToggleActionBuilder("Show Function Variables By Default", plugin.getName()) + .popupMenuPath("Function", "Show Variables By Default") + .popupMenuGroup("Visibility", "5") .helpLocation(new HelpLocation("CodeBrowserPlugin", "Show_All_Variables")) .selected(true) .withContext(ProgramLocationActionContext.class) .onAction(c -> showVariablesForAllFunctions(toggleVariablesAction.isSelected())) .buildAndInstallLocal(this); - new ActionBuilder("Toggle Show Function Variables", plugin.getName()) + new ActionBuilder("Show/Hide Function Variables", plugin.getName()) .popupMenuPath("Function", "Show/Hide Variables") - .popupMenuGroup("Variables") + .popupMenuGroup("Visibility", "4") .helpLocation(new HelpLocation("CodeBrowserPlugin", "Show_Variables")) .keyBinding("SPACE") .withContext(ProgramLocationActionContext.class) - .validWhen(this::isInFunctionArea) - .enabledWhen(this::isInFunctionArea) + .validWhen(this::isInFunctionVariablesArea) + .enabledWhen(this::isInFunctionVariablesArea) .onAction(c -> toggleShowVariables(c.getAddress())) .buildAndInstallLocal(this); @@ -506,10 +532,49 @@ public class CodeViewerProvider extends NavigatableComponentProviderAdapter } + private void showAllFunctions(boolean selected) { + ListingModel model = listingPanel.getListingModel(); + model.setAllFunctionsOpen(selected); + } + + private void toggleShowFunction(ProgramLocationActionContext context) { + Address cuAddress = context.getAddress(); + Function function = program.getListing().getFunctionContaining(cuAddress); + if (function == null) { + return; + } + ListingModel model = listingPanel.getListingModel(); + Address functionAddress = function.getEntryPoint(); + boolean open = model.isFunctionOpen(functionAddress); + + model.setFunctionOpen(functionAddress, !open); + if (context.getLocation() instanceof FunctionSignatureFieldLocation) { + // no need to move cursor + return; + } + if (!open) { + setLocation(new ProgramLocation(program, cuAddress)); + } + else { + // We have to use the CollapsedCodeLocation in this case, any other goto to + // an address that is collapse will open it up, which we don't want since + // we just closed it. Also, we need to correct to the start of the range since + // that is the address that will have the CollapsedField + Address corrected = adjustToStartOfContainingRange(function, cuAddress); + setLocation(new CollapsedCodeLocation(program, corrected)); + } + } + + private Address adjustToStartOfContainingRange(Function function, Address cuAddress) { + AddressSetView body = function.getBody(); + AddressRange range = body.getRangeContaining(cuAddress); + return range == null ? function.getEntryPoint() : range.getMinAddress(); + } + private void toggleShowVariables(Address address) { ListingModel model = listingPanel.getListingModel(); boolean open = model.areFunctionVariablesOpen(address); - model.setAllFunctionVariablesOpen(!open); + model.setFunctionVariablesOpen(address, !open); setLocation(new VariablesOpenCloseLocation(program, address)); } @@ -518,12 +583,28 @@ public class CodeViewerProvider extends NavigatableComponentProviderAdapter model.setAllFunctionVariablesOpen(selected); } - private boolean isInFunctionArea(ProgramLocationActionContext context) { + private boolean isInFunctionVariablesArea(ProgramLocationActionContext context) { ProgramLocation location = context.getLocation(); - return location instanceof FunctionLocation || + return location instanceof VariableLocation || location instanceof VariablesOpenCloseLocation; } + private boolean isInCollapsableCodeArea(ProgramLocationActionContext context) { + ProgramLocation location = context.getLocation(); + + // this allows the code collapse to be toggled on instructions in the body of a function, + // but we have to exclude the variable locations so as to not interfere with the + // open/close variables action which also is mapped to + if (location instanceof CodeUnitLocation && !(location instanceof VariableLocation) && + !(location instanceof VariablesOpenCloseLocation)) { + return true; + } + + return location instanceof FunctionSignatureFieldLocation || + location instanceof FunctionOpenCloseLocation || + location instanceof CollapsedCodeLocation; + } + private void buildQuickTogleFieldActions() { List quickToggleFieldNames = formatMgr.getQuickToggleFieldNames(); int count = 0; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/AbstractOpenCloseField.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/AbstractOpenCloseField.java index ae32eefb3c..6d54d9e965 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/AbstractOpenCloseField.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/AbstractOpenCloseField.java @@ -19,6 +19,7 @@ import java.awt.*; import docking.widgets.fieldpanel.support.*; import generic.theme.GIcon; +import generic.theme.Gui; import ghidra.app.util.viewer.proxy.EmptyProxy; import ghidra.app.util.viewer.proxy.ProxyObj; @@ -26,6 +27,7 @@ import ghidra.app.util.viewer.proxy.ProxyObj; * FactoryField class for displaying the open/close field. */ public abstract class AbstractOpenCloseField implements ListingField { + protected static final Font HIDDEN_FONT = Gui.getFont("font.listing.base.hidden.field"); protected static final GIcon OPEN_ICON = new GIcon("icon.base.util.viewer.fieldfactory.openclose.open"); protected static final GIcon CLOSED_ICON = diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/CollapsedCodeFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/CollapsedCodeFieldFactory.java new file mode 100644 index 0000000000..b23cd4bcc7 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/CollapsedCodeFieldFactory.java @@ -0,0 +1,107 @@ +/* ### + * 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.app.util.viewer.field; + +import java.math.BigInteger; + +import docking.widgets.fieldpanel.field.*; +import docking.widgets.fieldpanel.support.FieldLocation; +import ghidra.app.util.ListingHighlightProvider; +import ghidra.app.util.viewer.format.FieldFormatModel; +import ghidra.app.util.viewer.listingpanel.ListingModel; +import ghidra.app.util.viewer.proxy.CodeUnitProxy; +import ghidra.app.util.viewer.proxy.ProxyObj; +import ghidra.framework.options.Options; +import ghidra.framework.options.ToolOptions; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.*; +import ghidra.program.util.CollapsedCodeLocation; +import ghidra.program.util.ProgramLocation; + +/** + * Generates field to indicate collapsed function for areas of hidden code. + */ +public class CollapsedCodeFieldFactory extends FieldFactory { + public static final String FIELD_NAME = "Collapsed Code"; + + public CollapsedCodeFieldFactory() { + super(FIELD_NAME); + } + + /** + * Constructor + * @param model the model that the field belongs to. + * @param hlProvider the HightLightStringProvider. + * @param displayOptions the Options for display properties. + * @param fieldOptions the Options for field specific properties. + */ + private CollapsedCodeFieldFactory(FieldFormatModel model, ListingHighlightProvider hlProvider, + Options displayOptions, Options fieldOptions) { + super(FIELD_NAME, model, hlProvider, displayOptions, fieldOptions); + } + + @Override + public ListingField getField(ProxyObj proxy, int varWidth) { + if (!enabled || !(proxy instanceof CodeUnitProxy cuProxy)) { + return null; + } + CodeUnit cu = cuProxy.getObject(); + Address address = cu.getAddress(); + Function function = cu.getProgram().getListing().getFunctionContaining(address); + if (function == null) { + return null; + } + String text = ""; + AttributedString s = new AttributedString(text, ListingColors.COLLAPSED_CODE, getMetrics()); + FieldElement element = new TextFieldElement(s, 0, 0); + return ListingTextField.createSingleLineTextField(this, cuProxy, element, startX + varWidth, + width, hlProvider); + } + + @Override + public ProgramLocation getProgramLocation(int row, int col, ListingField bf) { + Object obj = bf.getProxy().getObject(); + if (obj instanceof CodeUnit cu) { + Address address = cu.getAddress(); + ListingModel layoutModel = bf.getProxy().getListingLayoutModel(); + Program program = layoutModel.getProgram(); + return new CollapsedCodeLocation(program, address); + } + return null; + } + + @Override + public FieldLocation getFieldLocation(ListingField bf, BigInteger index, int fieldNum, + ProgramLocation programLoc) { + if (programLoc instanceof CollapsedCodeLocation) { + return new FieldLocation(index, fieldNum, 0, 0); + } + return null; + } + + @Override + public boolean acceptsType(int category, Class proxyObjectClass) { + // this factory only appears in the header DIVIDER tab (which is called "Address Break" in + // the gui) + return category == FieldFormatModel.DIVIDER; + } + + @Override + public FieldFactory newInstance(FieldFormatModel formatModel, ListingHighlightProvider provider, + ToolOptions displayOptions, ToolOptions fieldOptions) { + return new CollapsedCodeFieldFactory(formatModel, provider, displayOptions, fieldOptions); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/CollapsedCodeFieldMouseHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/CollapsedCodeFieldMouseHandler.java new file mode 100644 index 0000000000..aa3ea9468d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/CollapsedCodeFieldMouseHandler.java @@ -0,0 +1,69 @@ +/* ### + * 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.app.util.viewer.field; + +import java.awt.event.MouseEvent; + +import ghidra.app.nav.Navigatable; +import ghidra.app.services.GoToService; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.program.util.*; + +/** + * A handler to process {@link CollapsedCodeLocation} mouse clicks. + */ +public class CollapsedCodeFieldMouseHandler implements FieldMouseHandlerExtension { + + private final static Class[] SUPPORTED_CLASSES = new Class[] { + CollapsedCodeLocation.class }; + + @Override + public boolean fieldElementClicked(Object clickedObject, Navigatable navigatable, + ProgramLocation location, MouseEvent mouseEvent, ServiceProvider serviceProvider) { + + if (mouseEvent.getClickCount() != 2 || mouseEvent.getButton() != MouseEvent.BUTTON1) { + return false; + } + + GoToService goToService = serviceProvider.getService(GoToService.class); + if (goToService == null) { + return false; + } + + Program program = location.getProgram(); + + if (location instanceof CollapsedCodeLocation loc) { + Address address = loc.getAddress(); + Function function = program.getListing().getFunctionContaining(address); + + if (function != null) { + FunctionSignatureFieldLocation gotoLocation = + new FunctionSignatureFieldLocation(program, function.getEntryPoint()); + goToService.goTo(navigatable, gotoLocation, program); + return true; + } + } + return false; + } + + @Override + public Class[] getSupportedProgramLocations() { + return SUPPORTED_CLASSES; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/FunctionOpenCloseField.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/FunctionOpenCloseField.java new file mode 100644 index 0000000000..f2965e66fc --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/FunctionOpenCloseField.java @@ -0,0 +1,93 @@ +/* ### + * 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.app.util.viewer.field; + +import java.awt.*; + +import javax.swing.JComponent; + +import docking.widgets.fieldpanel.internal.FieldBackgroundColorManager; +import docking.widgets.fieldpanel.internal.PaintContext; +import docking.widgets.fieldpanel.support.RowColLocation; +import ghidra.app.util.viewer.proxy.FunctionProxy; +import ghidra.app.util.viewer.proxy.ProxyObj; +import ghidra.program.model.address.Address; + +/** + * Field class for displaying the open/close field widget for function variables. + */ +public class FunctionOpenCloseField extends AbstractOpenCloseField { + + /** + * Constructor + * @param factory the FieldFactory that created this field. + * @param proxy the object associated with this field. + * @param metrics the FontMetrics used to render this field. + * @param x the starting x position of this field. + * @param width the width of this field. + */ + public FunctionOpenCloseField(FieldFactory factory, ProxyObj proxy, + FontMetrics metrics, int x, int width) { + super(factory, proxy, metrics, x, width); + if (proxy instanceof FunctionProxy functionProxy) { + Address functionAddress = functionProxy.getFunctionAddress(); + this.isOpen = proxy.getListingLayoutModel().isFunctionOpen(functionAddress); + } + } + + @Override + public int getWidth() { + return fieldWidth; + } + + @Override + public void paint(JComponent c, Graphics g, PaintContext context, + Rectangle clip, FieldBackgroundColorManager map, RowColLocation cursorLoc, + int rowHeight) { + + // center in the heightAbove area (negative, since 0 is the baseline of text, which is at + // the bottom of the heightAbove) + int toggleHandleStartY = -((heightAbove / 2) + (toggleHandleSize / 2)); + int toggleHandleStartX = startX; + + // If we're in printing mode, trying to render these open/close images + // causes the JVM to bomb. We'd like to eventually figure out why but in + // the meantime we can safely comment this out and still generate an acceptable + // image. + if (!context.isPrinting()) { + if (isOpen) { + g.drawImage(OPEN_ICON.getImageIcon().getImage(), toggleHandleStartX, + toggleHandleStartY, context.getBackground(), null); + } + else { + g.drawImage(CLOSED_ICON.getImageIcon().getImage(), toggleHandleStartX, + toggleHandleStartY, context.getBackground(), null); + } + } + paintCursor(g, context.getCursorColor(), cursorLoc); + } + + /** + * Toggles the open state of this field. + */ + @Override + public void toggleOpenCloseState() { + if (proxy instanceof FunctionProxy functionProxy) { + Address functionAddress = functionProxy.getFunctionAddress(); + proxy.getListingLayoutModel().setFunctionOpen(functionAddress, !isOpen); + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/FunctionOpenCloseFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/FunctionOpenCloseFieldFactory.java new file mode 100644 index 0000000000..d720046083 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/FunctionOpenCloseFieldFactory.java @@ -0,0 +1,110 @@ +/* ### + * 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.app.util.viewer.field; + +import java.math.BigInteger; + +import docking.widgets.fieldpanel.support.FieldLocation; +import ghidra.app.util.ListingHighlightProvider; +import ghidra.app.util.viewer.format.FieldFormatModel; +import ghidra.app.util.viewer.proxy.FunctionProxy; +import ghidra.app.util.viewer.proxy.ProxyObj; +import ghidra.framework.options.Options; +import ghidra.framework.options.ToolOptions; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.program.util.FunctionOpenCloseLocation; +import ghidra.program.util.ProgramLocation; + +/** + * Generates Open/Close Fields for variables under functions. + */ +public class FunctionOpenCloseFieldFactory extends FieldFactory { + + public static final String FIELD_NAME = "+"; + + public FunctionOpenCloseFieldFactory() { + super(FIELD_NAME); + } + + /** + * Constructor + * @param model the model that the field belongs to. + * @param hlProvider the HightLightStringProvider. + * @param displayOptions the Options for display properties. + * @param fieldOptions the Options for field specific properties. + */ + private FunctionOpenCloseFieldFactory(FieldFormatModel model, + ListingHighlightProvider hlProvider, + Options displayOptions, Options fieldOptions) { + super(FIELD_NAME, model, hlProvider, displayOptions, fieldOptions); + servicesChanged(); + } + + /** + * Returns the FactoryField for the given object at index index. + * @param varWidth the amount of variable width spacing for any fields + * before this one. + * @param proxy the object whose properties should be displayed. + */ + @Override + public ListingField getField(ProxyObj proxy, int varWidth) { + if (!enabled) { + return null; + } + if (proxy instanceof FunctionProxy) { + return new FunctionOpenCloseField(this, proxy, getMetrics(), startX + varWidth, width); + } + return null; + } + + @Override + public ProgramLocation getProgramLocation(int row, int col, ListingField bf) { + ProxyObj proxy = bf.getProxy(); + if (proxy instanceof FunctionProxy functionProxy) { + Program program = functionProxy.getProgram(); + Address functionAddress = functionProxy.getFunctionAddress(); + return new FunctionOpenCloseLocation(program, functionAddress); + } + return null; + } + + @Override + public FieldLocation getFieldLocation(ListingField bf, BigInteger index, int fieldNum, + ProgramLocation programLoc) { + + if (!(programLoc instanceof FunctionOpenCloseLocation)) { + return null; + } + return new FieldLocation(index, fieldNum, 0, 0); + } + + @Override + public boolean acceptsType(int category, Class proxyObjectClass) { + if (!Function.class.isAssignableFrom(proxyObjectClass)) { + return false; + } + return (category == FieldFormatModel.FUNCTION); + } + + @Override + public FieldFactory newInstance(FieldFormatModel fieldModel, ListingHighlightProvider provider, + ToolOptions displayOptions, ToolOptions fieldOptions) { + return new FunctionOpenCloseFieldFactory(fieldModel, provider, displayOptions, + fieldOptions); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingColors.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingColors.java index b557e55280..22679ea6a2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingColors.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingColors.java @@ -38,6 +38,7 @@ public class ListingColors { public static final GColor EXT_REF_RESOLVED = new GColor("color.fg.listing.ext.ref.resolved"); public static final GColor REGISTER = new GColor("color.fg.listing.register"); public static final GColor PARALLEL_INSTRUCTION = new GColor("color.fg.listing.instruction.parallel"); + public static final GColor COLLAPSED_CODE = new GColor("color.fg.listing.collapsed"); public static class XrefColors { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/OpenCloseFieldMouseHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/OpenCloseFieldMouseHandler.java index a5602efbf5..b62264f88b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/OpenCloseFieldMouseHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/OpenCloseFieldMouseHandler.java @@ -21,9 +21,13 @@ import ghidra.app.nav.Navigatable; import ghidra.framework.plugintool.ServiceProvider; import ghidra.program.util.ProgramLocation; +/** + * Mouse handlers for open/close widgets + */ public class OpenCloseFieldMouseHandler implements FieldMouseHandlerExtension { private final static Class[] SUPPORTED_CLASSES = - new Class[] { OpenCloseField.class, VariableOpenCloseField.class }; + new Class[] { OpenCloseField.class, VariableOpenCloseField.class, + FunctionOpenCloseField.class }; @Override public boolean fieldElementClicked(Object clickedObject, Navigatable sourceNavigatable, diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/SeparatorFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/SeparatorFieldFactory.java index f190364001..8fce27fc3b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/SeparatorFieldFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/SeparatorFieldFactory.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. @@ -22,6 +22,7 @@ import docking.widgets.fieldpanel.support.FieldLocation; import ghidra.app.util.ListingHighlightProvider; import ghidra.app.util.viewer.format.FieldFormatModel; import ghidra.app.util.viewer.listingpanel.ListingModel; +import ghidra.app.util.viewer.proxy.AddressProxy; import ghidra.app.util.viewer.proxy.ProxyObj; import ghidra.framework.options.Options; import ghidra.framework.options.ToolOptions; @@ -65,6 +66,9 @@ public class SeparatorFieldFactory extends FieldFactory { if (!enabled) { return null; } + if (!(proxy instanceof AddressProxy)) { + return null; + } int numChars = width / getMetrics().charWidth(sepChar); sb.setLength(0); for (int i = 0; i < numChars; i++) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/VariableOpenCloseField.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/VariableOpenCloseField.java index 19e675a6ed..76eab7b7fb 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/VariableOpenCloseField.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/VariableOpenCloseField.java @@ -23,7 +23,6 @@ import docking.widgets.fieldpanel.internal.FieldBackgroundColorManager; import docking.widgets.fieldpanel.internal.PaintContext; import docking.widgets.fieldpanel.support.RowColLocation; import generic.theme.GThemeDefaults.Colors.Palette; -import generic.theme.Gui; import ghidra.app.util.viewer.proxy.ProxyObj; import ghidra.app.util.viewer.proxy.VariableProxy; import ghidra.program.model.address.Address; @@ -32,7 +31,6 @@ import ghidra.program.model.address.Address; * FactoryField class for displaying the open/close field widget for function variables. */ public class VariableOpenCloseField extends AbstractOpenCloseField { - private static final Font HIDDEN_FONT = Gui.getFont("font.listing.base.hidden.field"); /** * Constructor diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/format/FormatManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/format/FormatManager.java index 73bc310f82..59ce6ce5d6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/format/FormatManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/format/FormatManager.java @@ -384,15 +384,26 @@ public class FormatManager implements OptionsChangeListener { Element rowElem = new Element("ROW"); Element colElem = new Element("FIELD"); - colElem.setAttribute("NAME", "Separator"); colElem.setAttribute("WIDTH", "80"); colElem.setAttribute("ENABLED", "true"); - rowElem.addContent(colElem); root.addContent(rowElem); - return root; + rowElem = new Element("ROW"); + colElem = new Element("FIELD"); + colElem.setAttribute("WIDTH", "150"); + colElem.setAttribute("ENABLED", "true"); + rowElem.addContent(colElem); + + colElem = new Element("FIELD"); + colElem.setAttribute("NAME", "Collapsed Code"); + colElem.setAttribute("WIDTH", "200"); + colElem.setAttribute("ENABLED", "true"); + rowElem.addContent(colElem); + + root.addContent(rowElem); + return root; } private Element getDefaultPlateFormat() { @@ -434,6 +445,12 @@ public class FormatManager implements OptionsChangeListener { Element rowElem = new Element("ROW"); Element colElem = new Element("FIELD"); + colElem.setAttribute("NAME", "+"); + colElem.setAttribute("WIDTH", "20"); + colElem.setAttribute("ENABLED", "true"); + rowElem.addContent(colElem); + + colElem = new Element("FIELD"); colElem.setAttribute("WIDTH", "200"); colElem.setAttribute("ENABLED", "true"); rowElem.addContent(colElem); @@ -961,7 +978,7 @@ public class FormatManager implements OptionsChangeListener { models[i].restoreFromXml(saveState.getXmlElement(models[i].getName())); // hack to make sure the new open/close variables field is present // If missing, we are just going to reset it to the default format - checkForMissingOpenCloseField(models[i]); + checkForMissingNewCriticalFields(models[i]); } else { models[i].restoreFromXml(getDefaultModel(i)); @@ -971,21 +988,30 @@ public class FormatManager implements OptionsChangeListener { modelChanged(null); } - // This is a hack to make sure the new variables open/close field is present. - // This was added in version 12.0 and can probably be removed in a few releases. - private void checkForMissingOpenCloseField(FieldFormatModel model) { - if (!model.getName().equals("Variable")) { - return; + // This is a hack to make sure the new variables and functions open/close field is present. + // This was added in version 12.0/12.1 and can probably be removed in a few releases. + private void checkForMissingNewCriticalFields(FieldFormatModel model) { + if (model.getName().equals("Variable")) { + if (!hasField(model, "+")) { + model.restoreFromXml(getDefaultVariableFormat()); + } } - if (!hasField(model, "+")) { - model.restoreFromXml(getDefaultVariableFormat()); + if (model.getName().equals("Function")) { + if (!hasField(model, "+")) { + model.restoreFromXml(getDefaultFunctionFormat()); + } + } + if (model.getName().equals("Address Break")) { + if (!hasField(model, "Collapsed Code")) { + model.restoreFromXml(getDefaultDividerFormat()); + } } } private boolean hasField(FieldFormatModel model, String fieldName) { FieldFactory[] unusedFactories = model.getUnusedFactories(); for (FieldFactory fieldFactory : unusedFactories) { - if (fieldFactory.getFieldName().equals("+")) { + if (fieldFactory.getFieldName().equals(fieldName)) { return false; } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/EmptyListingModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/EmptyListingModel.java index 9191175f36..b69376ebe7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/EmptyListingModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/EmptyListingModel.java @@ -142,4 +142,19 @@ public class EmptyListingModel implements ListingModel { public ListingModel copy() { return new EmptyListingModel(); } + + @Override + public boolean isFunctionOpen(Address functionAddress) { + return false; + } + + @Override + public void setFunctionOpen(Address functionAddress, boolean b) { + // stub + } + + @Override + public void setAllFunctionsOpen(boolean selected) { + // stub + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ListingModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ListingModel.java index d2ebb1bdd1..86da1ee052 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ListingModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ListingModel.java @@ -38,12 +38,39 @@ public interface ListingModel { FUNCTION_POINTER_OPTION_GROUP_NAME + Options.DELIMITER + "Display Non-External Function Pointer Header"; + /** + * {@return the address set of all addresses in the model} + */ public AddressSetView getAddressSet(); + /** + * Returns the next address that has displayable information after the given address. This + * allows the listing to efficiently skip over large sections of undisplayable addresses such + * as those consumed by large data or addresses part of a closed function. + * @param address the address from which to find the next address with displayable information + * @return the next address with displayable information + */ public Address getAddressAfter(Address address); + /** + * Returns the previous address that has displayable information before the given address. This + * allows the listing to efficiently skip over large sections of undisplayable addresses such + * as those consumed by large data or addresses part of a closed function. + * @param address the address from which to find the previous address with displayable + * information + * @return the previous address with displayable information + */ public Address getAddressBefore(Address address); + /** + * Returns a layout with displayable information for the given address. + * @param address the address to get displayable information + * @param isGapAddress true implies there is a gap of missing addresses before this address. + * Note that this is different from addresses that are hidden due to collapsed functions or + * closed data. These gaps are not even in consideration to display information such as + * undefined memory or a fragmented program view. + * @return a Layout with information to be displayed for the given address. + */ public Layout getLayout(Address address, boolean isGapAddress); /** @@ -181,4 +208,24 @@ public interface ListingModel { * @return a copy of this model. */ public ListingModel copy(); + + /** + * Checks if the function at the given entry point is open or not. + * @param functionAddress the entry point of the function to check for open + * @return true if the function is open; false otherwise + */ + public boolean isFunctionOpen(Address functionAddress); + + /** + * Sets the function at the given address to be open or not + * @param functionAddress the entry point of the function to set open or closed + * @param open true to open the function, false to close it + */ + public void setFunctionOpen(Address functionAddress, boolean open); + + /** + * Sets all functions to open or closed. + * @param open if true, opens all function; otherwise closes all functions + */ + public void setAllFunctionsOpen(boolean open); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ListingPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ListingPanel.java index 8e8776d315..0797acceb9 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ListingPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ListingPanel.java @@ -679,7 +679,7 @@ public class ListingPanel extends JPanel implements FieldMouseListener, FieldLoc return null; } - openDataAsNeeded(loc); + openDataOrFunctionAsNeeded(loc); FieldLocation floc = layoutModel.getFieldLocation(loc); if (floc != null) { @@ -719,15 +719,40 @@ public class ListingPanel extends JPanel implements FieldMouseListener, FieldLoc return layoutModel.getFieldLocation(new ProgramLocation(program, address)); } - private void openDataAsNeeded(ProgramLocation location) { + private void openDataOrFunctionAsNeeded(ProgramLocation location) { + if (location instanceof CollapsedCodeLocation) { + return; + } Address address = location.getByteAddress(); Program program = getProgram(); CodeUnit cu = program.getListing().getCodeUnitContaining(address); - if (!(cu instanceof Data)) { - return; + if (cu instanceof Data data) { + openData(data, address); + } + else if (cu instanceof Instruction instruction) { + openFunction(instruction); } - Data data = (Data) cu; + } + + private void openFunction(Instruction instruction) { + Address address = instruction.getMinAddress(); + Program program = instruction.getProgram(); + Function function = program.getFunctionManager().getFunctionContaining(address); + if (function == null) { + return; + } + Address functionAddress = function.getEntryPoint(); + // don't auto-open entry point addresses + if (address.equals(functionAddress)) { + return; + } + if (!listingModel.isFunctionOpen(functionAddress)) { + listingModel.setFunctionOpen(functionAddress, true); + } + } + + private void openData(Data data, Address address) { if (data.getComponent(0) == null) { // not sub data to open return; @@ -741,6 +766,7 @@ public class ListingPanel extends JPanel implements FieldMouseListener, FieldLoc if (openAllData(subData)) { layoutModel.dataChanged(true); } + } private FieldLocation getFieldLocationForDataAndOpenAsNeeded(Data data, Address address) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ProgramBigListingModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ProgramBigListingModel.java index 60769c89f1..1f9bfebf5a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ProgramBigListingModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ProgramBigListingModel.java @@ -43,7 +43,7 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener DomainObjectListener, ChangeListener, OptionsChangeListener { protected final Program program; - private ProgramOpenCloseManager openCloseMgr = new ProgramOpenCloseManager(); + private ProgramOpenCloseManager openCloseMgr; private FormatManager formatMgr; private ToolOptions fieldOptions; private boolean showExternalFunctionPointerFormat; @@ -59,6 +59,7 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener this.program = program; this.listing = program.getListing(); this.formatMgr = formatMgr; + openCloseMgr = new ProgramOpenCloseManager(program); dummyFactory = new DummyFieldFactory(formatMgr); formatMgr.addFormatModelListener(this); program.addListener(this); @@ -160,33 +161,43 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener addOpenData(dataList, data, addr); addUnionPostOpenData(dataList, data, addr); } + boolean isClosedFunctionAddress = isClosedFunctionAddress(function, addr); if (isGapAddress) { format = formatMgr.getDividerModel(); format.addLayouts(list, 0, new AddressProxy(this, addr)); } - if (cu != null) { + if (cu != null && function != null) { format = formatMgr.getPlateFormat(); format.addLayouts(list, 0, new CodeUnitProxy(this, program, cu)); } if (function != null) { format = formatMgr.getFunctionFormat(); format.addLayouts(list, 0, new FunctionProxy(this, program, addr, function)); - format = formatMgr.getFunctionVarFormat(); - boolean variablesOpen = openCloseMgr.isFunctionVariablesOpen(function.getEntryPoint()); - if (variablesOpen) { - addReturn(addr, list, format, function); - addParameters(addr, list, format, function); - addLocals(addr, list, format, function); + if (!isClosedFunctionAddress) { + format = formatMgr.getFunctionVarFormat(); + boolean variablesOpen = + openCloseMgr.isFunctionVariablesOpen(function.getEntryPoint()); + if (variablesOpen) { + addReturn(addr, list, format, function); + addParameters(addr, list, format, function); + addLocals(addr, list, format, function); + } + else { + format.addLayouts(list, 0, + new ClosedVariableProxy(this, program, addr, function)); + } } - else { - format.addLayouts(list, 0, new ClosedVariableProxy(this, program, addr, function)); - } - } if (cu != null) { - format = formatMgr.getCodeUnitFormat(); - format.addLayouts(list, 0, new CodeUnitProxy(this, program, cu)); + if (isClosedFunctionAddress) { + format = formatMgr.getDividerModel(); + } + else { + format = formatMgr.getCodeUnitFormat(); + } + CodeUnitProxy proxy = new CodeUnitProxy(this, program, cu); + format.addLayouts(list, 0, proxy); } if (dataList != null) { @@ -213,6 +224,16 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener } + private boolean isClosedFunctionAddress(Function function, Address addr) { + if (function == null) { + function = listing.getFunctionContaining(addr); + } + if (function == null) { + return false; + } + return !openCloseMgr.isFunctionOpen(function.getEntryPoint()); + } + private void addReturn(Address addr, List list, FieldFormatModel format, Function function) { format.addLayouts(list, 0, @@ -257,6 +278,35 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener return formatMgr.getMaxWidth(); } + @Override + public Address getAddressBefore(Address addr) { + CodeUnit cu = listing.getCodeUnitContaining(addr); + if (cu instanceof Data && !addr.equals(cu.getMinAddress()) && isOpenData(cu)) { + Address prevAddr = findOpenDataBefore(addr, (Data) cu); + if (prevAddr != null) { + return prevAddr; + } + return cu.getMinAddress(); + } + + cu = listing.getCodeUnitBefore(addr); + if (cu == null) { + return null; + } + if (isOpenData(cu)) { + return cu.getMaxAddress(); + } + if (!isInClosedFunction(cu)) { + return cu.getAddress(); + } + Address firstAddress = getFirstAddressInClosedFunctionRange(cu); + cu = listing.getCodeUnitAt(firstAddress); + if (!isInClosedFunction(cu)) { + return cu.getMinAddress(); + } + return getAddressBefore(firstAddress); + } + @Override public Address getAddressAfter(Address address) { CodeUnit cu = listing.getCodeUnitContaining(address); @@ -272,7 +322,51 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener } } cu = listing.getCodeUnitAfter(address); - return cu == null ? null : cu.getMinAddress(); + if (cu == null) { + return null; + } + if (!isInClosedFunction(cu)) { + return cu.getAddress(); + } + Address lastAddress = getLastAddressInClosedFunctionRange(cu); + return getAddressAfter(lastAddress); + } + + private Address getLastAddressInClosedFunctionRange(CodeUnit cu) { + Address address = cu.getAddress(); + Function f = listing.getFunctionContaining(address); + AddressSetView body = f.getBody(); + AddressRange range = body.getRangeContaining(address); + return range.getMaxAddress(); + } + + private Address getFirstAddressInClosedFunctionRange(CodeUnit cu) { + Address address = cu.getAddress(); + Function f = listing.getFunctionContaining(address); + AddressSetView body = f.getBody(); + AddressRange range = body.getRangeContaining(address); + return range.getMinAddress(); + } + + private boolean isInClosedFunction(CodeUnit cu) { + Address a = cu.getAddress(); + Function f = listing.getFunctionContaining(a); + if (f == null) { + return false; + } + Address entryPoint = f.getEntryPoint(); + if (a.equals(entryPoint)) { + return false; + } + AddressSetView body = f.getBody(); + AddressRange rangeContaining = body.getRangeContaining(a); + if (rangeContaining.getMinAddress().equals(a)) { + return false; + } + if (openCloseMgr.isFunctionOpen(entryPoint)) { + return false; + } + return true; } private Address findOpenDataAfter(Address address, Data parent) { @@ -344,25 +438,6 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener return null; } - @Override - public Address getAddressBefore(Address addr) { - CodeUnit cu = listing.getCodeUnitContaining(addr); - if (cu == null || addr.equals(cu.getMinAddress())) { - cu = listing.getCodeUnitBefore(addr); - if (isOpenData(cu)) { - return cu.getMaxAddress(); - } - return cu == null ? null : cu.getMinAddress(); - } - if (isOpenData(cu)) { - Address prevAddr = findOpenDataBefore(addr, (Data) cu); - if (prevAddr != null) { - return prevAddr; - } - } - return cu.getMinAddress(); - } - public boolean isOpenData(CodeUnit cu) { if (cu instanceof Data) { Data data = (Data) cu; @@ -491,8 +566,8 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener } @Override - public void setFunctionVariablesOpen(Address functionAddress, boolean open) { - openCloseMgr.setFunctionVariablesOpen(functionAddress, open); + public void setFunctionVariablesOpen(Address functionEntry, boolean open) { + openCloseMgr.setFunctionVariablesOpen(functionEntry, open); } @Override @@ -501,8 +576,23 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener } @Override - public boolean areFunctionVariablesOpen(Address FunctionAddress) { - return openCloseMgr.isFunctionVariablesOpen(FunctionAddress); + public boolean areFunctionVariablesOpen(Address functionEntry) { + return openCloseMgr.isFunctionVariablesOpen(functionEntry); + } + + @Override + public boolean isFunctionOpen(Address functionEntry) { + return openCloseMgr.isFunctionOpen(functionEntry); + } + + @Override + public void setFunctionOpen(Address functionEntry, boolean b) { + openCloseMgr.setFunctionOpen(functionEntry, b); + } + + @Override + public void setAllFunctionsOpen(boolean selected) { + openCloseMgr.setAllFunctionsOpen(selected); } @Override @@ -647,4 +737,5 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener model.openCloseMgr = openCloseMgr; return model; } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/multilisting/ListingModelConverter.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/multilisting/ListingModelConverter.java index 83558a3155..842bf4504b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/multilisting/ListingModelConverter.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/multilisting/ListingModelConverter.java @@ -212,4 +212,19 @@ public class ListingModelConverter implements ListingModel { return new ListingModelConverter(primaryModel.copy(), model.copy()); } + @Override + public boolean isFunctionOpen(Address functionAddress) { + return model.isFunctionOpen(functionAddress); + } + + @Override + public void setFunctionOpen(Address functionAddress, boolean b) { + model.setFunctionOpen(functionAddress, b); + } + + @Override + public void setAllFunctionsOpen(boolean selected) { + model.setAllFunctionsOpen(selected); + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/multilisting/MultiListingLayoutModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/multilisting/MultiListingLayoutModel.java index c04de749a2..b1ee82c66d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/multilisting/MultiListingLayoutModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/multilisting/MultiListingLayoutModel.java @@ -352,6 +352,21 @@ public class MultiListingLayoutModel implements ListingModelListener, FormatMode return models[modelID].copy(); } + @Override + public boolean isFunctionOpen(Address functionAddress) { + return models[modelID].isFunctionOpen(functionAddress); + } + + @Override + public void setFunctionOpen(Address functionAddress, boolean b) { + models[modelID].setFunctionOpen(functionAddress, b); + } + + @Override + public void setAllFunctionsOpen(boolean selected) { + models[modelID].setAllFunctionsOpen(selected); + } + } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/ListingDisplayOptionsEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/ListingDisplayOptionsEditor.java index a2389af420..6965e3a90b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/ListingDisplayOptionsEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/ListingDisplayOptionsEditor.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. @@ -61,7 +61,8 @@ public class ListingDisplayOptionsEditor implements OptionsEditor { options.registerThemeColorBinding(colorOptionName, element.getThemeColorId(), null, prefix + colorOptionName); String styleOptionName = element.getStyleOptionName(); - options.registerOption(styleOptionName, -1, null, prefix + styleOptionName); + int style = element.getStyle(); + options.registerOption(styleOptionName, style, null, prefix + styleOptionName); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/OptionsGui.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/OptionsGui.java index 62a1474ac1..3aac11872b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/OptionsGui.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/OptionsGui.java @@ -83,6 +83,7 @@ public class OptionsGui extends JPanel { public static final ScreenElement FLOW_ARROW_SELECTED = new ScreenElement("Flow Arrow, Selected", FlowArrowColors.SELECTED); public static final ScreenElement LABELS_PRIMARY = new ScreenElement("Labels, Primary", LabelColors.PRIMARY); public static final ScreenElement LABELS_NON_PRIMARY = new ScreenElement("Labels, Non-primary", LabelColors.NON_PRIMARY); + public static final ScreenElement COLLAPSED_CODE = new ScreenElement("Collapsed Code", ListingColors.COLLAPSED_CODE, Font.ITALIC); public static final ScreenElement COMMENT_PLATE = new ScreenElement("Comment, Plate", "Plate Comment", CommentColors.PLATE); public static final ScreenElement COMMENT_POST = new ScreenElement("Comment, Post", "Post-Comment", CommentColors.POST); public static final ScreenElement COMMENT_PRE = new ScreenElement("Comment, Pre", "Pre-Comment", CommentColors.PRE); @@ -99,10 +100,9 @@ public class OptionsGui extends JPanel { public static final ScreenElement PCODE_ADDR_SPACE = new ScreenElement("P-code Address Space", PcodeColors.ADDRESS_SPACE); public static final ScreenElement PCODE_RAW_VARNODE = new ScreenElement("P-code Raw Varnode", PcodeColors.VARNODE); public static final ScreenElement PCODE_USEROP = new ScreenElement("P-code Userop", PcodeColors.USEROP); - static ScreenElement[] elements = { ADDRESS, - BACKGROUND, BAD_REF_ADDR, BYTES, + BACKGROUND, BAD_REF_ADDR, BYTES, COLLAPSED_CODE, COMMENT_AUTO, COMMENT_EOL, COMMENT_PLATE, COMMENT_POST, COMMENT_PRE, COMMENT_REPEATABLE, COMMENT_REF_REPEAT, CONSTANT, ENTRY_POINT, EXT_REF_RESOLVED, EXT_REF_UNRESOLVED, @@ -716,6 +716,26 @@ public class OptionsGui extends JPanel { lb.add(" ", null); list.add(lb.getLayout()); + lb = new LayoutBuilder(1); + lb.add(" /***********************************/", COMMENT_PLATE); + list.add(lb.getLayout()); + lb = new LayoutBuilder(1); + lb.add(" /* PLATE */", COMMENT_PLATE); + list.add(lb.getLayout()); + lb = new LayoutBuilder(1); + lb.add(" /***********************************/", COMMENT_PLATE); + list.add(lb.getLayout()); + lb = new LayoutBuilder(4); + lb.add(" ", null); + lb.add("Word ", FUN_RET_TYPE); + lb.add("AnotherFunc", FUN_NAME); + lb.add("(", SEPARATOR); + lb.add(")", SEPARATOR); + list.add(lb.getLayout()); + lb = new LayoutBuilder(1); + lb.add("", COLLAPSED_CODE); + list.add(lb.getLayout()); + layouts = new Layout[list.size()]; list.toArray(layouts); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/ScreenElement.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/ScreenElement.java index 80ccc844c3..2ef274e8c6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/ScreenElement.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/ScreenElement.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. @@ -27,15 +27,23 @@ public class ScreenElement { private int style; ScreenElement(String name, GColor defaultColor) { - this(name, name, defaultColor); + this(name, name, defaultColor, -1); + } + + ScreenElement(String name, GColor defaultColor, int style) { + this(name, name, defaultColor, style); } ScreenElement(String name, String optionsName, GColor defaultColor) { + this(name, optionsName, defaultColor, -1); + } + + ScreenElement(String name, String optionsName, GColor defaultColor, int style) { this.name = name; this.optionsName = optionsName; this.defaultColor = defaultColor; this.color = defaultColor; - this.style = -1; + this.style = style; } public String getThemeColorId() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/proxy/FunctionProxy.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/proxy/FunctionProxy.java index ba6acc1429..b000643333 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/proxy/FunctionProxy.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/proxy/FunctionProxy.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. @@ -109,4 +109,8 @@ public class FunctionProxy extends ProxyObj { } return f.getBody().contains(a); } + + public Program getProgram() { + return program; + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/AddressBasedOpenCloseManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/InMemoryOpenCloseManager.java similarity index 98% rename from Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/AddressBasedOpenCloseManager.java rename to Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/InMemoryOpenCloseManager.java index 08fcc7edf7..61f9a7ac02 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/AddressBasedOpenCloseManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/InMemoryOpenCloseManager.java @@ -25,7 +25,7 @@ import ghidra.program.model.address.Address; * state can be set and then a set of address is kept for the locations that are the opposite * of the default. */ -public class AddressBasedOpenCloseManager { +public class InMemoryOpenCloseManager { private boolean openByDefault = true; private Set
addresses = new HashSet<>(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/PersistentOpenCloseManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/PersistentOpenCloseManager.java new file mode 100644 index 0000000000..782e76911a --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/PersistentOpenCloseManager.java @@ -0,0 +1,161 @@ +/* ### + * 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.app.util.viewer.util; + +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.program.model.listing.ProgramUserData; +import ghidra.program.model.util.VoidPropertyMap; + +/** + * Address based open/close management that uses the {@link ProgramUserData} to persist the + * open/close state for that address. Currently used for persisting the open/close state + * of functions in the listing. + */ +public class PersistentOpenCloseManager { + private boolean openByDefault = true; + private VoidPropertyMap booleanProperty; + private ProgramUserData programUserData; + + // Often, isOpen will be called on the same function address many times in a row so cache + // the last address and result + private Address cachedAddress; + private boolean cachedResult; + private String defaultOpenClosePropertyname; + + public PersistentOpenCloseManager(Program program, String owner, String propertyName) { + this.defaultOpenClosePropertyname = propertyName + "Default"; + programUserData = program.getProgramUserData(); + + int tx = programUserData.startTransaction(); + try { + booleanProperty = + programUserData.getBooleanProperty(owner, propertyName, true); + } + finally { + programUserData.endTransaction(tx); + } + + // Get the default open state. Only addresses different from default have properties stored. + String functionState = + programUserData.getStringProperty(defaultOpenClosePropertyname, "Open"); + openByDefault = functionState.equals("Open"); + } + + /** + * Checks if the state is "open" for the given address. + * @param address the address to test + * @return true if the state of the given address is "open" + */ + public boolean isOpen(Address address) { + if (address.equals(cachedAddress)) { + return cachedResult; + } + cachedAddress = address; + boolean contains = booleanProperty.hasProperty(address); + cachedResult = openByDefault ? !contains : contains; + return cachedResult; + } + + /** + * Sets the state at the given address to be "open". + * @param address the address to set "open" + */ + public void open(Address address) { + cachedAddress = null; + if (openByDefault) { + removeAddressProperty(address); + } + else { + addAddressProperty(address); + } + } + + /** + * Sets the state at the given address to be "closed". + * @param address the address to set "closed" + */ + public void close(Address address) { + cachedAddress = null; + if (openByDefault) { + addAddressProperty(address); + } + else { + removeAddressProperty(address); + } + } + + /** + * Checks if the default state is "open". + * @return true if the default state for addresses is "open" + */ + public boolean isOpenByDefault() { + return openByDefault; + } + + /** + * Sets all address to "open" (Makes "open" the default state and clears all individual + * settings. + */ + public void openAll() { + cachedAddress = null; + openByDefault = true; + clearProperties(); + programUserData.setStringProperty(defaultOpenClosePropertyname, "Open"); + } + + /** + * Sets all address to "closed" (Makes "closed" the default state and clears all individual + * settings. + */ + public void closeAll() { + cachedAddress = null; + openByDefault = false; + clearProperties(); + programUserData.setStringProperty(defaultOpenClosePropertyname, "Closed"); + } + + private void addAddressProperty(Address address) { + int tx = programUserData.startTransaction(); + try { + booleanProperty.add(address); + } + finally { + programUserData.endTransaction(tx); + } + } + + private void removeAddressProperty(Address address) { + int tx = programUserData.startTransaction(); + try { + booleanProperty.remove(address); + } + finally { + programUserData.endTransaction(tx); + } + } + + private void clearProperties() { + int tx = programUserData.startTransaction(); + try { + booleanProperty.clear(); + } + finally { + programUserData.endTransaction(tx); + } + + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/ProgramOpenCloseManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/ProgramOpenCloseManager.java index 05bbb53226..6c067beac2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/ProgramOpenCloseManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/ProgramOpenCloseManager.java @@ -33,30 +33,42 @@ import ghidra.util.task.TaskMonitor; public class ProgramOpenCloseManager { private DataOpenCloseManager dataOpenCloseManager = new DataOpenCloseManager(); private List listeners = new ArrayList<>(); - private AddressBasedOpenCloseManager variablesOpenCloseManager = new AddressBasedOpenCloseManager(); + private InMemoryOpenCloseManager variablesOpenCloseManager; + private PersistentOpenCloseManager functionsOpenCloseManager; + + public ProgramOpenCloseManager(Program program) { + // open/close functions states are persisted and specific to a program + functionsOpenCloseManager = new PersistentOpenCloseManager(program, + getClass().getSimpleName(), "Function Open State"); + + // open/close variables states are tool based so users can + // choose whether to see them by default. They can be opened or closed + // for specific functions in specific program, but this is not persisted for the program. + variablesOpenCloseManager = new InMemoryOpenCloseManager(); + } /** * Sets whether or not to display function variables at the given address. - * @param functionAddress the address of the function + * @param functionEntry the address of the function * @param open true to display function variables, false to to hide them */ - public void setFunctionVariablesOpen(Address functionAddress, boolean open) { + public void setFunctionVariablesOpen(Address functionEntry, boolean open) { if (open) { - variablesOpenCloseManager.open(functionAddress); + variablesOpenCloseManager.open(functionEntry); } else { - variablesOpenCloseManager.close(functionAddress); + variablesOpenCloseManager.close(functionEntry); } notifyListeners(); } /** * Checks if the function variables are being shown at the given function address. - * @param functionAddress the address of the function to check + * @param functionEntry the address of the function to check * @return true if the variables are being displayed */ - public boolean isFunctionVariablesOpen(Address functionAddress) { - return variablesOpenCloseManager.isOpen(functionAddress); + public boolean isFunctionVariablesOpen(Address functionEntry) { + return variablesOpenCloseManager.isOpen(functionEntry); } /** @@ -74,8 +86,42 @@ public class ProgramOpenCloseManager { notifyListeners(); } - public boolean isAllFunctionVariablesOpen() { - return variablesOpenCloseManager.isOpenByDefault(); + /** + * Returns true if the function at the given address is open. + * @param functionEntry the function address to test if the function is open + * @return true if the function at the given address is open + */ + public boolean isFunctionOpen(Address functionEntry) { + return functionsOpenCloseManager.isOpen(functionEntry); + } + + /** + * Sets the function at the given address to be open or closed. + * @param functionEntry the address of the function to open or close + * @param open true to open the function, false to close it + */ + public void setFunctionOpen(Address functionEntry, boolean open) { + if (open) { + functionsOpenCloseManager.open(functionEntry); + } + else { + functionsOpenCloseManager.close(functionEntry); + } + notifyListeners(); + } + + /** + * Sets all functions to be open or closed. + * @param open true to open all functions, false to close them + */ + public void setAllFunctionsOpen(boolean open) { + if (open) { + functionsOpenCloseManager.openAll(); + } + else { + functionsOpenCloseManager.closeAll(); + } + notifyListeners(); } /** @@ -164,4 +210,5 @@ public class ProgramOpenCloseManager { l.stateChanged(null); } } + } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/util/PersistentOpenCloseManagerTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/util/PersistentOpenCloseManagerTest.java new file mode 100644 index 0000000000..0194ac8ccc --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/util/PersistentOpenCloseManagerTest.java @@ -0,0 +1,134 @@ +/* ### + * 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.app.util.viewer.util; + +import static org.junit.Assert.*; + +import org.junit.*; + +import ghidra.framework.model.*; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.listing.Program; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.test.TestEnv; +import ghidra.util.task.TaskMonitor; + +public class PersistentOpenCloseManagerTest extends AbstractGhidraHeadedIntegrationTest { + private DomainFile df; + private AddressSpace space; + private Program program; + private PersistentOpenCloseManager openCloseMgr; + private TestEnv env; + + @Before + public void setUp() throws Exception { + env = new TestEnv(); + Project project = env.getProject(); + ProgramBuilder builder = new ProgramBuilder("Test", ProgramBuilder._TOY, this); + ProjectData projectData = project.getProjectData(); + + DomainFolder rootFolder = projectData.getRootFolder(); + program = builder.getProgram(); + df = rootFolder.createFile("test", program, TaskMonitor.DUMMY); + + space = program.getAddressFactory().getDefaultAddressSpace(); + openCloseMgr = new PersistentOpenCloseManager(program, "test", "test"); + } + + @After + public void tearDown() { + env.dispose(); + } + + @Test + public void testOpenByDefaultCloseAllOpenAll() throws Exception { + assertTrue(openCloseMgr.isOpen(addr(0))); + assertTrue(openCloseMgr.isOpen(addr(100))); + assertTrue(openCloseMgr.isOpenByDefault()); + + openCloseMgr.closeAll(); + + assertFalse(openCloseMgr.isOpen(addr(0))); + assertFalse(openCloseMgr.isOpen(addr(100))); + assertFalse(openCloseMgr.isOpenByDefault()); + + openCloseMgr.openAll(); + assertTrue(openCloseMgr.isOpen(addr(0))); + assertTrue(openCloseMgr.isOpen(addr(100))); + assertTrue(openCloseMgr.isOpenByDefault()); + } + + @Test + public void testCloseAndOpenSpecificAddresses() throws Exception { + assertTrue(openCloseMgr.isOpen(addr(0))); + assertTrue(openCloseMgr.isOpen(addr(100))); + + openCloseMgr.close(addr(0)); + + assertFalse(openCloseMgr.isOpen(addr(0))); + assertTrue(openCloseMgr.isOpen(addr(100))); + + openCloseMgr.open(addr(0)); + + assertTrue(openCloseMgr.isOpen(addr(0))); + assertTrue(openCloseMgr.isOpen(addr(100))); + } + + @Test + public void testOpenAndCloseSpecificAddressesWithDefaultClosed() throws Exception { + openCloseMgr.closeAll(); + + assertFalse(openCloseMgr.isOpen(addr(0))); + assertFalse(openCloseMgr.isOpen(addr(100))); + + openCloseMgr.open(addr(0)); + + assertTrue(openCloseMgr.isOpen(addr(0))); + assertFalse(openCloseMgr.isOpen(addr(100))); + + openCloseMgr.close(addr(0)); + + assertFalse(openCloseMgr.isOpen(addr(0))); + assertFalse(openCloseMgr.isOpen(addr(100))); + } + + @Test + public void testPersistence() throws Exception { + openCloseMgr.closeAll(); + assertFalse(openCloseMgr.isOpen(addr(0))); + assertFalse(openCloseMgr.isOpen(addr(100))); + + openCloseMgr.open(addr(0)); + assertTrue(openCloseMgr.isOpen(addr(0))); + assertFalse(openCloseMgr.isOpen(addr(100))); + + program.release(this); + program = (Program) df.getDomainObject(this, false, false, TaskMonitor.DUMMY); + space = program.getAddressFactory().getDefaultAddressSpace(); + openCloseMgr = new PersistentOpenCloseManager(program, "test", "test"); + + assertTrue(openCloseMgr.isOpen(addr(0))); + assertFalse(openCloseMgr.isOpen(addr(100))); + assertFalse(openCloseMgr.isOpenByDefault()); + } + + private Address addr(long offset) { + return space.getAddress(offset); + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/util/viewer/listingpanel/ProgramBigListingModelTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/util/viewer/listingpanel/ProgramBigListingModelTest.java new file mode 100644 index 0000000000..d836f7b0b2 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/util/viewer/listingpanel/ProgramBigListingModelTest.java @@ -0,0 +1,162 @@ +/* ### + * 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.app.util.viewer.listingpanel; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; + +import generic.test.AbstractGenericTest; +import ghidra.app.util.viewer.format.FormatManager; +import ghidra.framework.options.SaveState; +import ghidra.framework.options.ToolOptions; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.database.ProgramDB; +import ghidra.program.database.function.OverlappingFunctionException; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Function; + +public class ProgramBigListingModelTest extends AbstractGenericTest { + + private ProgramDB program; + private ProgramBigListingModel model; + private AddressSpace space; + + @Before + public void setUp() throws Exception { + ProgramBuilder builder = new ProgramBuilder("Test", ProgramBuilder._X86); + program = builder.getProgram(); + space = program.getAddressFactory().getDefaultAddressSpace(); + builder.createMemory("block1", "0", 18); + buildFun0(builder); + + FormatManager formatManager = + new FormatManager(new ToolOptions("display"), new ToolOptions("Listing Fields")); + formatManager.readState(new SaveState()); // this will use default format + model = new ProgramBigListingModel(program, formatManager); + } + + private void buildFun0(ProgramBuilder builder) throws OverlappingFunctionException { + Function fun0 = builder.createFunction("0"); + AddressSet addrs = new AddressSet(); + addrs.add(addr(0), addr(3)); + addrs.add(addr(6), addr(9)); + addrs.add(addr(12), addr(15)); + program.withTransaction("test", () -> fun0.setBody(addrs)); + } + + private Address addr(long offset) { + return space.getAddress(offset); + } + + @Test + public void testGetNextAddressWithFunctionOpen() { + assertEquals(addr(1), model.getAddressAfter(addr(0))); + assertEquals(addr(2), model.getAddressAfter(addr(1))); + assertEquals(addr(3), model.getAddressAfter(addr(2))); + assertEquals(addr(4), model.getAddressAfter(addr(3))); + assertEquals(addr(5), model.getAddressAfter(addr(4))); + assertEquals(addr(6), model.getAddressAfter(addr(5))); + assertEquals(addr(7), model.getAddressAfter(addr(6))); + assertEquals(addr(8), model.getAddressAfter(addr(7))); + assertEquals(addr(9), model.getAddressAfter(addr(8))); + assertEquals(addr(10), model.getAddressAfter(addr(9))); + assertEquals(addr(11), model.getAddressAfter(addr(10))); + assertEquals(addr(12), model.getAddressAfter(addr(11))); + assertEquals(addr(13), model.getAddressAfter(addr(12))); + assertEquals(addr(14), model.getAddressAfter(addr(13))); + assertEquals(addr(15), model.getAddressAfter(addr(14))); + assertEquals(addr(16), model.getAddressAfter(addr(15))); + assertEquals(addr(17), model.getAddressAfter(addr(16))); + assertNull(model.getAddressAfter(addr(17))); + } + + @Test + public void testGetNextAddressWithFunctionClosed() { + model.setFunctionOpen(addr(0), false); + + assertEquals(addr(4), model.getAddressAfter(addr(0))); + assertEquals(addr(4), model.getAddressAfter(addr(1))); + assertEquals(addr(4), model.getAddressAfter(addr(2))); + assertEquals(addr(4), model.getAddressAfter(addr(3))); + assertEquals(addr(5), model.getAddressAfter(addr(4))); + assertEquals(addr(6), model.getAddressAfter(addr(5))); + assertEquals(addr(10), model.getAddressAfter(addr(6))); + assertEquals(addr(10), model.getAddressAfter(addr(7))); + assertEquals(addr(10), model.getAddressAfter(addr(8))); + assertEquals(addr(10), model.getAddressAfter(addr(9))); + assertEquals(addr(11), model.getAddressAfter(addr(10))); + assertEquals(addr(12), model.getAddressAfter(addr(11))); + assertEquals(addr(16), model.getAddressAfter(addr(12))); + assertEquals(addr(16), model.getAddressAfter(addr(13))); + assertEquals(addr(16), model.getAddressAfter(addr(14))); + assertEquals(addr(16), model.getAddressAfter(addr(15))); + assertEquals(addr(17), model.getAddressAfter(addr(16))); + assertNull(model.getAddressAfter(addr(17))); + } + + @Test + public void testGetPreviousAddressWithFunctionOpen() { + assertNull(model.getAddressBefore(addr(0))); + assertEquals(addr(0), model.getAddressBefore(addr(1))); + assertEquals(addr(1), model.getAddressBefore(addr(2))); + assertEquals(addr(2), model.getAddressBefore(addr(3))); + assertEquals(addr(3), model.getAddressBefore(addr(4))); + assertEquals(addr(4), model.getAddressBefore(addr(5))); + assertEquals(addr(5), model.getAddressBefore(addr(6))); + assertEquals(addr(6), model.getAddressBefore(addr(7))); + assertEquals(addr(7), model.getAddressBefore(addr(8))); + assertEquals(addr(8), model.getAddressBefore(addr(9))); + assertEquals(addr(9), model.getAddressBefore(addr(10))); + assertEquals(addr(10), model.getAddressBefore(addr(11))); + assertEquals(addr(11), model.getAddressBefore(addr(12))); + assertEquals(addr(12), model.getAddressBefore(addr(13))); + assertEquals(addr(13), model.getAddressBefore(addr(14))); + assertEquals(addr(14), model.getAddressBefore(addr(15))); + assertEquals(addr(15), model.getAddressBefore(addr(16))); + assertEquals(addr(16), model.getAddressBefore(addr(17))); + assertEquals(addr(17), model.getAddressBefore(addr(18))); + assertEquals(addr(17), model.getAddressBefore(addr(19))); + } + + @Test + public void testGetPreviousAddressWithFunctionClosed() { + model.setFunctionOpen(addr(0), false); + + assertNull(model.getAddressBefore(addr(0))); + assertEquals(addr(0), model.getAddressBefore(addr(1))); + assertEquals(addr(0), model.getAddressBefore(addr(2))); + assertEquals(addr(0), model.getAddressBefore(addr(3))); + assertEquals(addr(0), model.getAddressBefore(addr(4))); + assertEquals(addr(4), model.getAddressBefore(addr(5))); + assertEquals(addr(5), model.getAddressBefore(addr(6))); + assertEquals(addr(6), model.getAddressBefore(addr(7))); + assertEquals(addr(6), model.getAddressBefore(addr(8))); + assertEquals(addr(6), model.getAddressBefore(addr(9))); + assertEquals(addr(6), model.getAddressBefore(addr(10))); + assertEquals(addr(10), model.getAddressBefore(addr(11))); + assertEquals(addr(11), model.getAddressBefore(addr(12))); + assertEquals(addr(12), model.getAddressBefore(addr(13))); + assertEquals(addr(12), model.getAddressBefore(addr(14))); + assertEquals(addr(12), model.getAddressBefore(addr(15))); + assertEquals(addr(12), model.getAddressBefore(addr(16))); + assertEquals(addr(16), model.getAddressBefore(addr(17))); + assertEquals(addr(17), model.getAddressBefore(addr(18))); + assertEquals(addr(17), model.getAddressBefore(addr(19))); + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/properties/PropertyMapDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/properties/PropertyMapDB.java index 38f33ad473..7a0cfe1d1f 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/properties/PropertyMapDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/properties/PropertyMapDB.java @@ -213,6 +213,20 @@ public abstract class PropertyMapDB extends DatabaseObject implements Propert return name; } + @Override + public void clear() { + if (propertyTable == null) { + return; + } + cache = new ObjectCache<>(DEFAULT_CACHE_SIZE); + try { + propertyTable.deleteAll(); + } + catch (IOException e) { + errHandler.dbError(e); + } + } + /** * Adjust the size of the underlying read cache. * @param size the size of the cache. @@ -225,7 +239,6 @@ public abstract class PropertyMapDB extends DatabaseObject implements Propert finally { lock.release(); } - } /** diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/util/DefaultPropertyMap.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/util/DefaultPropertyMap.java index 99365915e3..927ec5615f 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/util/DefaultPropertyMap.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/util/DefaultPropertyMap.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. @@ -79,6 +79,21 @@ public abstract class DefaultPropertyMap implements PropertyMap { return description; } + @Override + public void clear() { + if (getSize() == 0) { + return; + } + try { + long first = propertyMgr.getFirstPropertyIndex(); + long last = propertyMgr.getLastPropertyIndex(); + propertyMgr.removeRange(first, last); + } + catch (NoSuchIndexException e) { + // can't happen since we checked it isn't empty + } + } + @Override public boolean intersects(Address start, Address end) { return propertyMgr.intersects(addrMap.getKey(start), addrMap.getKey(end)); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/util/PropertyMap.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/util/PropertyMap.java index 0e6a47e2f8..f810ee6307 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/util/PropertyMap.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/util/PropertyMap.java @@ -34,6 +34,11 @@ public interface PropertyMap { */ public Class getValueClass(); + /** + * Removes all properties from this map + */ + public void clear(); + /** * Given two addresses, indicate whether there is an address in * that range (inclusive) having the property. @@ -45,7 +50,7 @@ public interface PropertyMap { * has the property, false otherwise. */ public boolean intersects(Address start, Address end); - + /** * Indicate whether there is an address within * the set which exists within this map. @@ -55,7 +60,7 @@ public interface PropertyMap { * has the property, false otherwise. */ public boolean intersects(AddressSetView set); - + /** * Removes all property values within a given range. * @param start begin range @@ -64,6 +69,7 @@ public interface PropertyMap { * false otherwise. */ public boolean removeRange(Address start, Address end); + /** * Remove the property value at the given address. * @return true if the property value was removed, false @@ -71,7 +77,7 @@ public interface PropertyMap { * @param addr the address where the property should be removed */ public boolean remove(Address addr); - + /** * returns whether there is a property value at addr. * @param addr the address in question @@ -86,7 +92,7 @@ public interface PropertyMap { * @throws IllegalArgumentException if property value type is inappropriate for this map */ public void add(Address addr, Object value); - + /** * Returns the property value stored at the specified * address or null if no property found. @@ -94,39 +100,39 @@ public interface PropertyMap { * @return property value */ public T get(Address addr); - + /** * Get the next address where the property value exists. * @param addr the address from which to begin the search (exclusive). * @return property value location after specified {@code addr} or null if none found */ public Address getNextPropertyAddress(Address addr); - + /** * Get the previous Address where a property value exists. * @param addr the address from which to begin the search (exclusive). * @return property value location after specified {@code addr} or null if none found */ public Address getPreviousPropertyAddress(Address addr); - + /** * Get the first Address where a property value exists. * @return first property value location or null if none found */ public Address getFirstPropertyAddress(); - + /** * Get the last Address where a property value exists. * @return last property value location or null if none found */ public Address getLastPropertyAddress(); - + /** * Get the number of properties in the map. * @return number of stored property values */ public int getSize(); - + /** * Returns an iterator over the indices having a property value. * @param start minimum address @@ -134,7 +140,7 @@ public interface PropertyMap { * @return forward property address iterator */ public AddressIterator getPropertyIterator(Address start, Address end); - + /** * Returns an iterator over addresses that have a property value. * @param start minimum address @@ -144,7 +150,7 @@ public interface PropertyMap { * @return property address iterator */ public AddressIterator getPropertyIterator(Address start, Address end, boolean forward); - + /** * Returns an iterator over the addresses that a property value. * @return forward property address iterator @@ -157,7 +163,7 @@ public interface PropertyMap { * @param asv the set of addresses to iterate over. * @return forward property address iterator */ - public AddressIterator getPropertyIterator(AddressSetView asv); + public AddressIterator getPropertyIterator(AddressSetView asv); /** * Returns an iterator over the addresses that have a property value and @@ -168,7 +174,7 @@ public interface PropertyMap { * @return property address iterator */ public AddressIterator getPropertyIterator(AddressSetView asv, boolean forward); - + /** * Returns an iterator over the address having a property value. * @param start the starting address @@ -177,7 +183,7 @@ public interface PropertyMap { * @return property address iterator */ public AddressIterator getPropertyIterator(Address start, boolean forward); - + /** * Moves the properties defined in the range from the start address thru the * end address to now be located beginning at the newStart address. diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/CollapsedCodeLocation.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/CollapsedCodeLocation.java new file mode 100644 index 0000000000..0fa1532589 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/CollapsedCodeLocation.java @@ -0,0 +1,46 @@ +/* ### + * 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.util; + +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; + +/** + * CollapsedCodeLocation is program location generated when the cursor is on a field + * representing code from a collapsed function. + */ +public class CollapsedCodeLocation extends ProgramLocation { + + /** + * Create a new DividerLocation. + * + * @param program the program of the location + * @param addr address of bookmark + * by its hierarchy names; this parameter may be null + */ + public CollapsedCodeLocation(Program program, Address addr) { + super(program, addr, addr, null, null, 0, 0, 0); + } + + /** + * Default constructor needed for restoring + * a program location from XML + */ + public CollapsedCodeLocation() { + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/FunctionOpenCloseLocation.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/FunctionOpenCloseLocation.java new file mode 100644 index 0000000000..f2eb641a7c --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/FunctionOpenCloseLocation.java @@ -0,0 +1,41 @@ +/* ### + * 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.util; + +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; + +/** + * ProgramLocation that represents the cursor being on the function's open/close widget + */ +public class FunctionOpenCloseLocation extends CodeUnitLocation { + /** + * Constructor + * + * @param program the program of the location + * @param addr address of the location + */ + public FunctionOpenCloseLocation(Program program, Address addr) { + super(program, addr, null, 0, 0, 0); + } + + /** + * Required constructor for serialization + */ + public FunctionOpenCloseLocation() { + + } +}