diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java index afb0ce07a5..1f63cff305 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.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. @@ -778,10 +778,18 @@ public class DebuggerCoordinates { return coords; } - public boolean isAlive() { + public static boolean isAlive(Target target) { return target != null && target.isValid(); } + public boolean isAlive() { + return isAlive(target); + } + + public static boolean isAliveAndPresent(TraceProgramView view, Target target) { + return isAlive(target) && target.getSnap() == view.getSnap(); + } + protected boolean isPresent() { TraceSchedule defaultedTime = getTime(); return target.getSnap() == defaultedTime.getSnap() && defaultedTime.isSnapOnly(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerByteSource.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerByteSource.java new file mode 100644 index 0000000000..3080d36a0b --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerByteSource.java @@ -0,0 +1,112 @@ +/* ### + * 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.plugin.core.debug.gui; + +import java.util.List; +import java.util.concurrent.*; +import java.util.stream.Stream; + +import db.Transaction; +import ghidra.app.nav.Navigatable; +import ghidra.app.plugin.core.debug.gui.action.DebuggerReadsMemoryTrait; +import ghidra.debug.api.target.Target; +import ghidra.debug.api.tracemgr.DebuggerCoordinates; +import ghidra.features.base.memsearch.bytesource.AddressableByteSource; +import ghidra.features.base.memsearch.bytesource.SearchRegion; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryAccessException; +import ghidra.trace.model.memory.*; +import ghidra.trace.model.program.TraceProgramView; + +/** + * A byte source for searching the memory of a possibly-live target in the debugger. + * + *

+ * Because we'd like the search to preserve its state over the lifetime of the target, and the + * target "changes" by navigating snapshots, we need to allow the view to move without requiring a + * new byte source to be constructed. We cannot, however, just blindly follow the + * {@link Navigatable} wherever it goes. This is roughly the equivalent of a {@link Program}, but + * with knowledge of the target to cause a refresh of actual target memory when necessary. + */ +public class DebuggerByteSource implements AddressableByteSource { + + private final PluginTool tool; + private final TraceProgramView view; + private final Target target; + private final DebuggerReadsMemoryTrait readsMem; + + public DebuggerByteSource(PluginTool tool, TraceProgramView view, Target target, + DebuggerReadsMemoryTrait readsMem) { + this.tool = tool; + this.view = view; + this.target = target; + this.readsMem = readsMem; + } + + @Override + public int getBytes(Address address, byte[] bytes, int length) { + AddressSet set = new AddressSet(address, address.add(length - 1)); + try { + readsMem.getAutoSpec() + .readMemory(tool, DebuggerCoordinates.NOWHERE.view(view).target(target), set) + .get(Target.TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + return view.getMemory().getBytes(address, bytes, 0, length); + } + catch (AddressOutOfBoundsException | MemoryAccessException | InterruptedException + | ExecutionException | TimeoutException e) { + return 0; + } + } + + @Override + public List getSearchableRegions() { + AddressFactory factory = view.getTrace().getBaseAddressFactory(); + List spaces = Stream.of(factory.getPhysicalSpaces()) + .filter(s -> s.getType() != AddressSpace.TYPE_OTHER) + .toList(); + if (spaces.size() == 1) { + return DebuggerSearchRegionFactory.ALL.stream() + .map(f -> f.createRegion(null)) + .toList(); + } + + Stream concat = + Stream.concat(Stream.of((AddressSpace) null), spaces.stream()); + return concat + .flatMap(s -> DebuggerSearchRegionFactory.ALL.stream().map(f -> f.createRegion(s))) + .toList(); + } + + @Override + public void invalidate() { + try (Transaction tx = view.getTrace().openTransaction("Invalidate memory")) { + TraceMemoryManager mm = view.getTrace().getMemoryManager(); + for (AddressSpace space : view.getTrace().getBaseAddressFactory().getAddressSpaces()) { + if (!space.isMemorySpace()) { + continue; + } + TraceMemorySpace ms = mm.getMemorySpace(space, false); + if (ms == null) { + continue; + } + ms.setState(view.getSnap(), space.getMinAddress(), space.getMaxAddress(), + TraceMemoryState.UNKNOWN); + } + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerSearchRegionFactory.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerSearchRegionFactory.java new file mode 100644 index 0000000000..10e94f360e --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerSearchRegionFactory.java @@ -0,0 +1,130 @@ +/* ### + * 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.plugin.core.debug.gui; + +import java.util.List; + +import ghidra.features.base.memsearch.bytesource.SearchRegion; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryBlock; + +public enum DebuggerSearchRegionFactory { + FULL_SPACE("All Addresses", """ + Searches all memory in the space, regardless of known validity.""") { + + @Override + AddressSetView getAddresses(AddressSpace space, Program program) { + AddressSet set = new AddressSet(); + if (space != null) { + set.add(space.getMinAddress(), space.getMaxAddress()); + return set; + } + for (AddressSpace s : program.getAddressFactory().getAddressSpaces()) { + set.add(s.getMinAddress(), s.getMaxAddress()); + } + return set; + } + }, + VALID("Valid Addresses", """ + Searches listed memory regions in the space.""") { + + @Override + AddressSetView getAddresses(AddressSpace space, Program program) { + AddressSet set = new AddressSet(); + for (MemoryBlock block : program.getMemory().getBlocks()) { + if (space == null || space == block.getStart().getAddressSpace()) { + set.add(block.getAddressRange()); + } + } + return set; + } + + @Override + boolean isDefault(AddressSpace space) { + return space == null; + } + }, + WRITABLE("Writable Addresses", """ + Searches listed regions marked as writable in the space.""") { + + @Override + AddressSetView getAddresses(AddressSpace space, Program program) { + AddressSet set = new AddressSet(); + for (MemoryBlock block : program.getMemory().getBlocks()) { + if (block.isWrite() && + (space == null || space == block.getStart().getAddressSpace())) { + set.add(block.getAddressRange()); + } + } + return set; + } + }; + + public static final List ALL = List.of(values()); + + record DebuggerSearchRegion(DebuggerSearchRegionFactory factory, AddressSpace spaces) + implements SearchRegion { + @Override + public String getName() { + return factory.getName(spaces); + } + + @Override + public String getDescription() { + return factory.getDescription(spaces); + } + + @Override + public AddressSetView getAddresses(Program program) { + return factory.getAddresses(spaces, program); + } + + @Override + public boolean isDefault() { + return factory.isDefault(spaces); + } + } + + private final String namePrefix; + private final String description; + + private DebuggerSearchRegionFactory(String namePrefix, String description) { + this.namePrefix = namePrefix; + this.description = description; + } + + public SearchRegion createRegion(AddressSpace space) { + return new DebuggerSearchRegion(this, space); + } + + String getName(AddressSpace space) { + if (space == null) { + return namePrefix; + } + return "%s (%s)".formatted(namePrefix, space.getName()); + } + + String getDescription(AddressSpace spaces) { + return description; + } + + abstract AddressSetView getAddresses(AddressSpace space, Program program); + + boolean isDefault(AddressSpace space) { + return false; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java index f8a96384b7..211b88326f 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.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. @@ -51,8 +51,7 @@ import ghidra.app.plugin.core.codebrowser.MarkerServiceBackgroundColorModel; import ghidra.app.plugin.core.debug.disassemble.CurrentPlatformTraceDisassembleCommand; import ghidra.app.plugin.core.debug.disassemble.CurrentPlatformTraceDisassembleCommand.Reqs; import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPlugin; -import ghidra.app.plugin.core.debug.gui.DebuggerLocationLabel; -import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.*; import ghidra.app.plugin.core.debug.gui.DebuggerResources.FollowsCurrentThreadAction; import ghidra.app.plugin.core.debug.gui.DebuggerResources.OpenProgramAction; import ghidra.app.plugin.core.debug.gui.action.*; @@ -76,6 +75,8 @@ import ghidra.debug.api.listing.MultiBlendedListingBackgroundColorModel; import ghidra.debug.api.modules.DebuggerMissingModuleActionContext; import ghidra.debug.api.modules.DebuggerStaticMappingChangeListener; import ghidra.debug.api.tracemgr.DebuggerCoordinates; +import ghidra.features.base.memsearch.bytesource.AddressableByteSource; +import ghidra.features.base.memsearch.bytesource.EmptyByteSource; import ghidra.framework.model.DomainFile; import ghidra.framework.options.SaveState; import ghidra.framework.plugintool.*; @@ -1358,4 +1359,12 @@ public class DebuggerListingProvider extends CodeViewerProvider { .setViewerPosition(vp.getIndex(), vp.getXOffset(), vp.getYOffset()); }); } + + @Override + public AddressableByteSource getByteSource() { + if (current == DebuggerCoordinates.NOWHERE) { + return EmptyByteSource.INSTANCE; + } + return new DebuggerByteSource(tool, current.getView(), current.getTarget(), readsMemTrait); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProvider.java index 3931bce10e..1dddf5485c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesProvider.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. @@ -34,8 +34,7 @@ import docking.menu.MultiStateDockingAction; import docking.widgets.fieldpanel.support.ViewerPosition; import generic.theme.GThemeDefaults.Colors; import ghidra.app.plugin.core.byteviewer.*; -import ghidra.app.plugin.core.debug.gui.DebuggerLocationLabel; -import ghidra.app.plugin.core.debug.gui.DebuggerResources; +import ghidra.app.plugin.core.debug.gui.*; import ghidra.app.plugin.core.debug.gui.DebuggerResources.FollowsCurrentThreadAction; import ghidra.app.plugin.core.debug.gui.action.*; import ghidra.app.plugin.core.debug.gui.action.AutoReadMemorySpec.AutoReadMemorySpecConfigFieldCodec; @@ -46,6 +45,8 @@ import ghidra.app.services.DebuggerTraceManagerService; import ghidra.debug.api.action.GoToInput; import ghidra.debug.api.action.LocationTrackingSpec; import ghidra.debug.api.tracemgr.DebuggerCoordinates; +import ghidra.features.base.memsearch.bytesource.AddressableByteSource; +import ghidra.features.base.memsearch.bytesource.EmptyByteSource; import ghidra.framework.options.SaveState; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoConfigStateField; @@ -666,4 +667,12 @@ public class DebuggerMemoryBytesProvider extends ProgramByteViewerComponentProvi newProvider.panel.setViewerPosition(vp); }); } + + @Override + public AddressableByteSource getByteSource() { + if (current == DebuggerCoordinates.NOWHERE) { + return EmptyByteSource.INSTANCE; + } + return new DebuggerByteSource(tool, current.getView(), current.getTarget(), readsMemTrait); + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/memory/AbstractDBTraceMemoryManagerMemoryTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/memory/AbstractDBTraceMemoryManagerMemoryTest.java index 4af570456a..98f8bc8380 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/memory/AbstractDBTraceMemoryManagerMemoryTest.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/memory/AbstractDBTraceMemoryManagerMemoryTest.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. @@ -960,4 +960,35 @@ public abstract class AbstractDBTraceMemoryManagerMemoryTest assertArrayEquals(b.arr(1, 2, 3, 4), read.array()); } } + + @Test + public void testReplicateNpeScenario() throws Exception { + ByteBuffer buf4k = ByteBuffer.allocate(0x1000); + AddressSetView set = b.set( + b.range(0x00400000, 0x00404fff), + b.range(0x00605000, 0x00606fff), + b.range(0x7ffff7a2c000L, 0x7ffff7a33fffL)); + Random random = new Random(); + for (int i = 0; i < 30; i++) { + try (Transaction tx = b.startTransaction()) { + for (int j = 0; j < 3; j++) { + for (AddressRange r : set) { + for (AddressRange rc : new AddressRangeChunker(r, 0x1000)) { + if (random.nextInt(100) < 20) { + memory.setState(0, rc, TraceMemoryState.ERROR); + continue; + } + buf4k.position(0); + buf4k.limit(0x1000); + memory.putBytes(0, rc.getMinAddress(), buf4k); + } + } + } + } + + try (Transaction tx = b.startTransaction()) { + memory.setState(0, b.range(0, -1), TraceMemoryState.UNKNOWN); + } + } + } } diff --git a/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/util/database/spatial/RStarTreeMapTest.java b/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/util/database/spatial/RStarTreeMapTest.java index 94b92afbb9..0b832ef1a2 100644 --- a/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/util/database/spatial/RStarTreeMapTest.java +++ b/Ghidra/Debug/ProposedUtils/src/test/java/ghidra/util/database/spatial/RStarTreeMapTest.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. @@ -1097,6 +1097,23 @@ public class RStarTreeMapTest { assertTrue(obj.map.isEmpty()); } + @Test + public void testAddThenRemove1() { + assertTrue(obj.map.isEmpty()); + + try (Transaction tx = obj.openTransaction("AddPoint")) { + obj.map.put(new ImmutableIntRect(0, 0, 0, 10), "Test"); + } + + assertFalse(obj.map.isEmpty()); + + try (Transaction tx = obj.openTransaction("RemovePoint")) { + assertTrue(obj.map.remove(new ImmutableIntRect(0, 0, 0, 10), "Test")); + } + + assertTrue(obj.map.isEmpty()); + } + @Test public void testClear() { List> points = generatePoints(rect(1, 12, 1, 12)); diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index c053587a20..0b966dd28b 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -508,8 +508,10 @@ src/main/help/help/topics/RuntimeInfoPlugin/RuntimeInfo.htm||GHIDRA||||END| src/main/help/help/topics/ScalarSearchPlugin/The_Scalar_Table.htm||GHIDRA||||END| src/main/help/help/topics/ScalarSearchPlugin/images/ScalarWindow.png||GHIDRA||||END| src/main/help/help/topics/ScalarSearchPlugin/images/SearchAllScalarsDialog.png||GHIDRA||||END| +src/main/help/help/topics/Search/Instruction_Mnemonic_Search.htm||GHIDRA||||END| src/main/help/help/topics/Search/Query_Results_Dialog.htm||GHIDRA||||END| src/main/help/help/topics/Search/Regular_Expressions.htm||GHIDRA||||END| +src/main/help/help/topics/Search/Search_Formats.htm||GHIDRA||||END| src/main/help/help/topics/Search/Search_Instruction_Patterns.htm||GHIDRA||||END| src/main/help/help/topics/Search/Search_Memory.htm||GHIDRA||||END| src/main/help/help/topics/Search/Search_Program_Text.htm||GHIDRA||||END| @@ -520,6 +522,9 @@ src/main/help/help/topics/Search/Searching.htm||GHIDRA||||END| src/main/help/help/topics/Search/images/DirectReferences.png||GHIDRA||||END| src/main/help/help/topics/Search/images/EncodedStringsDialog_advancedoptions.png||GHIDRA||||END| src/main/help/help/topics/Search/images/EncodedStringsDialog_initial.png||GHIDRA||||END| +src/main/help/help/topics/Search/images/MemorySearchProvider.png||GHIDRA||||END| +src/main/help/help/topics/Search/images/MemorySearchProviderWithOptionsOn.png||GHIDRA||||END| +src/main/help/help/topics/Search/images/MemorySearchProviderWithScanPanelOn.png||GHIDRA||||END| src/main/help/help/topics/Search/images/MultipleSelectionError.png||GHIDRA||||END| src/main/help/help/topics/Search/images/QueryResultsSearch.png||GHIDRA||||END| src/main/help/help/topics/Search/images/SearchForAddressTables.png||GHIDRA||||END| @@ -536,11 +541,7 @@ src/main/help/help/topics/Search/images/SearchInstructionsIncludeOperands.png||G src/main/help/help/topics/Search/images/SearchInstructionsIncludeOperandsNoConsts.png||GHIDRA||||END| src/main/help/help/topics/Search/images/SearchInstructionsManualSearchDialog.png||GHIDRA||||END| src/main/help/help/topics/Search/images/SearchLimitExceeded.png||GHIDRA||||END| -src/main/help/help/topics/Search/images/SearchMemoryBinary.png||GHIDRA||||END| -src/main/help/help/topics/Search/images/SearchMemoryDecimal.png||GHIDRA||||END| -src/main/help/help/topics/Search/images/SearchMemoryHex.png||GHIDRA||||END| src/main/help/help/topics/Search/images/SearchMemoryRegex.png||GHIDRA||||END| -src/main/help/help/topics/Search/images/SearchMemoryString.png||GHIDRA||||END| src/main/help/help/topics/Search/images/SearchText.png||GHIDRA||||END| src/main/help/help/topics/Search/images/StringSearchDialog.png||GHIDRA||||END| src/main/help/help/topics/Search/images/StringSearchResults.png||GHIDRA||||END| @@ -919,6 +920,11 @@ src/main/resources/images/unlock.gif||GHIDRA||||END| src/main/resources/images/verticalSplit.png||GHIDRA||||END| src/main/resources/images/view-sort-ascending.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/resources/images/view-sort-descending.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| +src/main/resources/images/view_bottom.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END| +src/main/resources/images/view_left_right.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END| +src/main/resources/images/view_top_bottom.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END| +src/main/resources/images/viewmag+.png||Crystal Clear Icons - LGPL 2.1||||END| +src/main/resources/images/viewmag.png||GHIDRA||||END| src/main/resources/images/window.png||GHIDRA||||END| src/main/resources/images/wizard.png||Nuvola Icons - LGPL 2.1|||nuvola|END| src/main/resources/images/x-office-document-template.png||Tango Icons - Public Domain|||tango icon set|END| diff --git a/Ghidra/Features/Base/data/base.icons.theme.properties b/Ghidra/Features/Base/data/base.icons.theme.properties index 648f2fdb88..05628777bb 100644 --- a/Ghidra/Features/Base/data/base.icons.theme.properties +++ b/Ghidra/Features/Base/data/base.icons.theme.properties @@ -395,6 +395,11 @@ icon.base.util.xml.functions.bookmark = imported_bookmark.gif icon.base.util.datatree.version.control.archive.dt.checkout.undo = vcUndoCheckOut.png +icon.base.mem.search.panel.options = view_left_right.png +icon.base.mem.search.panel.scan = view_bottom.png +icon.base.mem.search.panel.search = view_top_bottom.png + + diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/Instruction_Mnemonic_Search.htm b/Ghidra/Features/Base/src/main/help/help/topics/Search/Instruction_Mnemonic_Search.htm new file mode 100644 index 0000000000..7a99d6113a --- /dev/null +++ b/Ghidra/Features/Base/src/main/help/help/topics/Search/Instruction_Mnemonic_Search.htm @@ -0,0 +1,112 @@ + + + + + + + Search Memory + + + + + + +

+ +

Search for Matching Instructions

+ +
+

This action works only on a selection of code. It uses the selected instructions to build + a combined mask/value bit pattern that is then used to populate the search field in a + Memory Search Window. This enables searching through memory + for a particular ordering of + instructions. There are three options available: 

+ + + +
+

Example:

+ +

A user first selects the following lines of code. Then, from the Search menu they choose + Search for Matching Instructions and one of the following options:

+ +

+ Option 1: + +
+

If the Include Operands action is chosen then the search will find all + instances of the following instructions and operands.

+ +

+

+ +

All of the bytes that make up the selected code will be searched for exactly, with no + wild carding. The bit pattern 10000101 11000000 01010110 01101010 00010100 + 01011110 which equates to the byte pattern 85 c0 56 6a 14 5e is searched + for.
+
+

+
Option 2: + +
+

If the Exclude Operands option is chosen then the search will find all + instances of the following instructions only.

+ +

+

+ +

Only the parts of the byte pattern that make up the instructions will be searched for + with the remaining bits used as wildcards. The bit pattern 10000101 11...... 01010... + 01101010 ........ 01011... is searched for where the .'s indicate the wild carded + values.
+
+

+
Option 3: + +
+

If the Include Operands (except constants) option is chosen then the search + will find all instances of the instruction and all operands except the 0x14 which is a + constant.

+ +

+ +

The bit pattern 10000101 11000000 01010110 01101010 ........ 01011110 which + equates to the byte pattern 85 c0 56 6a xx 5e is searched for where xx can be any + number N between 0x0 and 0xff.
+
+

+
+
+ +

NoteThe previous operations can only work on a + single selected region. If multiple regions are selected, the following error dialog + will be shown and the operation will be cancelled.

+ +

+
+
+ +

Provided by: Mnemonic Search Plugin

+ +

Related Topics:

+ +
+
+
+ + diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/Regular_Expressions.htm b/Ghidra/Features/Base/src/main/help/help/topics/Search/Regular_Expressions.htm index 34cbbacdbf..d3f0a4e88c 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/Search/Regular_Expressions.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/Search/Regular_Expressions.htm @@ -19,17 +19,21 @@ same as a regular expression for any standard Java application. Because of restrictions on how regular expressions are processed, regular expression searches can only be performed in the forward direction. Unlike standard string searches, case sensitivity and unicode options do not - apply. The Search Memory dialog below shows a sample regular expression entered in the + apply. The Search Memory window below shows a sample regular expression entered in the Value field.

- + + +
+
+
- +
- +

Examples

diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/Search_Formats.htm b/Ghidra/Features/Base/src/main/help/help/topics/Search/Search_Formats.htm new file mode 100644 index 0000000000..566d267613 --- /dev/null +++ b/Ghidra/Features/Base/src/main/help/help/topics/Search/Search_Formats.htm @@ -0,0 +1,1019 @@ + + + + + + + Search Memory + + + + + + +
+

Search Formats

+ +

The selected format determines how the user input is used to generate a search byte + sequence (and possibly mask byte sequence). They are also used to format bytes back into + "values" to be displayed in the table, if applicable.

+ +

Hex:

+ +
+

The hex format allows the user to specify the search bytes as hex values.

+ +

Notes:

+
+ + + +
+

Examples: (Little Endian)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
1212FF
12 A412 A4FF FF
12A4A4 12FF FF
12 345612 56 34FF FF FF
5 E1205 12 0EFF FF FF
5.50F0
.5050F
12.404 120F FF
+
+ +

Examples: (Big Endian)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
1212FF
12 A412 A4FF FF
12A412 A4FF FF
12 345612 34 56FF FF FF
5 E1205 0E 12FF FF FF
5.50F0
.5050F
12.412 04FF 0F
+
+
+ +

Binary:

+ +
+

The Binary format allows the user to specify the search bytes as binary + values.

+ +
+ + + +
+

Examples:

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
1000000181FF
1103FF
0 1 000 01 00FF FF FF
0 1000 02FF FF
111.00.0E0ED
1 . 001 00 00FF 00 FF
+
+
+ +

String:

+ +
+

The String format allows the user to search to specify the search bytes as a string.

+ +
+ + + +
+

Examples: (Encoding is Ascii, Case Sensitive is on, Escape Sequences is off)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
Hey048 65 79 30FF FF FF FF
Hey\n49 65 79 5c 6eFF FF FF FF FF
+
+ +

Examples: (Encoding is Ascii, Case Sensitive is off, Escape Sequences is off)

+ +
+ DF DF DF FF + + + + + + + + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
Hey048 45 59 30
Hey\n49 65 79 5c 6eDF DF DF DF FF DF
+
+ +

Examples: (Encoding is Ascii, Case Sensitive is on, Escape Sequences is on)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
Hey048 65 79 30FF FF FF FF
Hey\n49 65 79 0AFF FF FF FF
+
+

Examples: (Encoding is UTF-16, Case Sensitive is on, Escape Sequences is off, Little Endian)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
Hey48 00 65 00 70 00 79 00FF FF FF FF FF FF
a\n61 00 5c 00 6e 00FF FF FF FF FF FF
+
+ +

Examples: (Encoding is UTF-16, Case Sensitive is on, Escape Sequences is off, Big Endian)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
Hey00 48 00 65 00 70 00 79FF FF FF FF FF FF
a\n00 61 00 5c 00 6eFF FF FF FF FF FF
+
+
+ +

Reg Ex:

+ +
+

The Reg Ex format allows the user to search memory for strings using Java regular + expressions.

+ +
+ + + +

Decimal:

+ +
+

The Decimal format allows the user to search for a sequence of decimal values.

+ +
+ + + +
+

Examples: (Size = 1 byte, Signed Values)

+ +
+ + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
-1 0 127FF 0 7FFF FF FF
+
+ +

Examples: (Size = 2 byte, Signed Values, Little Endian)

+ +
+ + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
-1 0 32767FF FF 00 00 FF 7FFF FF FF FF FF FF
+
+ +

Examples: (Size = 2 byte, Signed Values, Big Endian)

+ +
+ + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
-1 0 32767FF FF 00 00 7F FFFF FF FF FF FF FF
+
+

Examples: (Size = 4 byte, Signed Values, Little Endian)

+ +
+ + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
-1 5FF FF FF FF 05 00 00 00FF FF FF FF FF FF FF FF
+
+ +

Examples: (Size = 4 byte, Signed Values, Big Endian)

+ +
+ + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
-1 5FF FF FF FF 00 00 00 05FF FF FF FF FF FF
+
+ +

Examples: (Size = 8 byte, Signed Values, Little Endian)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
-1FF FF FF FF FF FF FF FFFF FF FF FF FF FF FF FF
505 00 00 00 00 00 00 00FF FF FF FF FF FF FF FF
+
+ +

Examples: (Size = 8 byte, Signed Values, Big Endian)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
-1FF FF FF FF FF FF FF FFFF FF FF FF FF FF FF FF
500 00 00 00 00 00 00 05FF FF FF FF FF FF FF FF
+
+ +

Examples: (Size = 1 byte, Unsigned Values)

+ +
+ + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
0 2560 FFFF FF
+
+

Examples: (Size = 2 byte, Unsigned Values, Little Endian)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
505 00FF FF
65535FF FFFF FF
+
+ +

Examples: (Size = 2 byte, Unsigned Values, Big Endian)

+ +
+ + + + + + + + + + + + + + + + +
50 05FF FF
65535FF FFFF FF
+
+ +

Examples: (Size = 4 byte, Unsigned Values, Little Endian)

+ +
+ + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
505 00 00 00FF FF FF FF
+
+ +

Examples: (Size = 4 byte, Unsigned Values, Big Endian)

+ +
+ + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
500 00 00 05FF FF FF FF FF
+
+
+ + +
+

Examples: (Size = 8 byte, Unsigned Values, Little Endian)

+ +
+ + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
505 00 00 00 00 00 00 00FF FF FF FF FF FF FF FF
+
+ +

Examples: (Size = 8 byte, Unsigned Values, Big Endian)

+ +
+ + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
500 00 00 00 00 00 00 05FF FF FF FF FF FF FF FF
+
+
+ + +

Float:

+ +
+

The Float format allows the user to enter floating point numbers of size 4 bytes.

+ +
+ + + +
+

Examples: (Little Endian)

+ +
+ + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
3.14C3 F5 48 40FF FF FF FF
+
+

Examples: (Big Endian)

+
+ + + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
3.1440 48 F5 C3FF FF FF FF
+
+ +
+

Double:

+ +
+

The Double format allows the user to enter floating point numbers of size 8 bytes.

+ +
+ + + +
+

Examples: (Little Endian)

+ +
+ + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
3.141F 85 EB 51 B8 1E 09 40FF FF FF FF FF FF FF FF
+
+

Examples: (Big Endian)

+ +
+ + + + + + + + + + + + + + + + +
Input StringByte SequenceMask Bytes
3.1440 09 1E B8 51 EB 85 1FFF FF FF FF FF FF FF FF
+
+
+ +
+
+
+ + diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/Search_Memory.htm b/Ghidra/Features/Base/src/main/help/help/topics/Search/Search_Memory.htm index 5e53b35fc7..33cb787bc1 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/Search/Search_Memory.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/Search/Search_Memory.htm @@ -12,467 +12,525 @@ -

Search Memory

- -

Search Memory locates sequences of bytes in program memory.  The search is based on a - value entered as hex numbers, decimal numbers or strings.  The byte sequence may contain - "wildcards" that will match any byte (or possibly nibble). String searching also allows for the - use of regular expression searches.

- -

To Search Memory:

-
-
    -
  1. From the Tool, select Search - Memory
  2. + -
  3. Enter a Hex String in the Value field
    - This will create a Hex Sequence for searching.
  4. +

    Search Memory

    -
  5. Choose "Next" to find the next occurrence
    -                               - - or -
    - Choose "Previous" to find the previous occurrence
    -                               - - or -
    - Choose "Search All" to find all occurrences.
  6. -
-
+

The memory search feature locates sequences of bytes in program memory. The search is + based on user entered values which can be specified in a variety of formats such as hex, + binary, string, and decimal values. The hex and binary formats support "wildcards" which can + match any bit or nibbles depending on the format. String search also supports the use of regular expression searches.

- -

Search Formats

- -
- -
- -

 

- -

Search Options

- -
-

Search

+

To create a new memory search window, select Search Memory from the main tool menu or use the default keybinding + "S".

+

By default, search windows and their + tabs display "Search Memory:" followed by the search string and the program name. This + can be changed by right-clicking on the title or table to change its name. (This is true + for all transient windows.)

+

Contents

-

Search Value

-
- - - -
-

Hex Sequence

-
- - - -

Format

- -
-

Hex:

-
- - - -
-
-

Example:

- - - - - - - - - - - - - - - - - - - - - -
-

Value:   

-
-

 "1234 567 89ab"

-
-

Little Endian - Hex Sequence   

-
-

34 12 67 05 ab 89

-
-

Big Endian Hex - Sequence   

-
-

12 34 05 67 89 ab

-
- -

- As a convenience, if a user enters a single wildcard value within the search text, then - the search string will be interpreted as if 2 consecutive wildcard characters were - entered, meaning to match any byte value. -

-

- Similarly, if the search string contains an odd number of characters, then a 0 is prepended - to the search string, based on the assumption that a single hex digit implies a leading - 0 value. -

- - -
-
- -

 

- -
-

String:

- -
-

Value is interpreted as the specified character encoding. The center panel of the - Search Memory dialog shows the Format Options, described below.

- -

- -
    -
  • Encoding - Interprets strings by the specified encoding.  Note that - byte ordering determines if the high order byte comes first or last.
  • - -
  • Case Sensitive - Turning off this option will search for the string - regardless of case using the specified character encoding. Only applicable for English - characters.
  • - -
  • Escape Sequences - Enabling this option allows escape sequences in the - search value (i.e., allows \n to be searched for).
  • -
-
- -

Decimal:

- -
-

Value is interpreted as a sequence of decimal numbers, separated by spaces. The center - panel of the Search Memory dialog shows the Decimal Options, described below.

-
- -

- +
-

Binary:

+

Memory Search Window

-

Value is interpreted as a sequence of binary numbers, separated by spaces.  - Wildcard characters ('x' or '?' or '.') can be used to match any bit.

-
+

The Memory Search Window provides controls and options for entering search values and + and a table for displaying the search results. It supports both bulk searching and + incremental searching forwards and backwards. Also, users can perform additional searches + and combine those results with the existing results using set operations. The window also + has a "scan" feature which will re-examine the bytes of the current results set, looking + for memory changes at those locations (this is most useful when debugging). Scanning has an + option for reducing the results to those that either changed, didn't change, were + incremented, or were decremented.

-

+

 

- - -

Regular Expression:

- -
-

Value is interpreted as a Java Regular Expression - that is matched against memory as if all memory was a string. Help on how to form regular - expressions is available on the Regular Expression - Help page.

-
- -

- - -
- -

Memory Block Types

- - - -

 

- -

Selection Scope

- - - -

 

- -

Code Unit Scope

- -
-

Filters the matches based upon the code unit containing a given address.

- - -
- -

Byte Order

- -
-

Sets the byte ordering for multi-byte values.  Has no effect on non-Unicode Ascii - values, Binary, or regular expressions.

- -

Little Endian - places low-order bytes first.
- For example, the hex number "1234" will generate the bytes "34" , "12".

- -

Big Endian - places high-order bytes first.
- For example, the hex number "1234" will generate the bytes "12", "34".

-
- -

Alignment

- - - -

 

- -

Searching

- - - -
-

For very large Programs that may take a - while to search, you can cancel the search at any time. For these situations, a progress bar - is displayed, along with a Cancel button. Click on the Cancel button to stop the search. 

- -

Dismissing the search dialog - automatically cancels the search operation.

-
- -

 

- -

Highlight Search Option

- -
-

You can specify that the bytes found in the search be highlighted in the Code Browser by - selecting the Highlight Search Results checkbox on the Search Options panel. To view - the Search Options, select Edit - Tool Options... from the tool menu, then select the Search node in the Options - tree in the Options dialog. You can also change the highlight color. Click on the color bar - next to Highlight Color to bring up a color chooser. Choose the new color, click on - the OK button. Apply your changes by clicking on the OK or Apply button - on the Options dialog. 

- -
-

Highlights are displayed for the last - search that you did. For example, if you bring up the Search Program Text dialog and search - for text, that string now becomes the new highlight string. Similarly, if you invoke cursor text - highlighting, that becomes the new highlight string.

-
- -

Highlights are dropped when you close the search dialog, or close the query results window - for your most recent search.

- -

- -
- -

Search for Matching Instructions

- -
-

This action works only on a selection of code. It uses the selected instructions to build - a combined mask/value bit pattern that is then used to populate the search field in the - Memory Search Dialog. This enables searching through memory for a particular ordering of - instructions. There are three options available: 

- - - -
-

Example:

- -

A user first selects the following lines of code. Then, from the Search menu they choose - Search for Matching Instructions and one of the following options:

- -

- Option 1: +

Search Controls

-

If the Include Operands action is chosen then the search will find all - instances of the following instructions and operands.

+

At the top of the window as shown above, there are several GUI elements for initializing and + executing a memory byte search. These controls can be closed from the view after a search + to give more space to view results using the toolbar button.

-

-

+

Search Format Field:

-

All of the bytes that make up the selected code will be searched for exactly, with no - wild carding. The bit pattern 10000101 11000000 01010110 01101010 00010100 - 01011110 which equates to the byte pattern 85 c0 56 6a 14 5e is searched - for.
-
-

-
Option 2: +
+

This is a drop-down list of formats whose selected format determines how to + interpret the text in the Search Input Field. The format will convert the user's + input into a sequence of bytes (and possibly masks). Details on each format are + described in the Search Formats section.

+
+ +

Search Input Field:

+ +
+

Next to the Search Format drop-down, there is a text field where users can + enter one or more values to be searched. This field performs validation depending on + the active format. For example, when the format is Hex, users can only enter + hexadecimal values.

+
+ +

Previous Search Drop Down:

+ +
+

At the end of the input field, there is a drop-down list of previous searches. + Selecting one of these will populate the input field with that previous search input, + as well as the relevant settings that were used in that search such as the search + format.

+
+ +

Search Button:

+ +
+

Pressing the search button will initiate a search. When the results table is empty, + the only choice is to do an initial search. If there are current results showing in the + table, a drop-down will appear at the back of the button, allowing the user to combine + new search results with the existing results using set operations. See the + Combining Searches section + below for more details.

+
+ +

Byte Sequence Field:

+ +
+

This field is used to show the user the bytes sequence that will be search for based + on the format and the user input entered. Hovering on this field will also display the + masks that will be used (if applicable).

+
+ +

Selection Only Checkbox:

+ +
+

If there is a current selection, then this checkbox will be enabled and provide the + user with the option to restrict the search to only the selected addresses. Note that + there is an action that controls whether this option will be selected automatically if + a selection exists.

+
+
+ +

Scan Controls

-

If the Exclude Operands option is chosen then the search will find all - instances of the following instructions only.

+

The scan controls are used to re-examine search results, looking for values that have + changed since the search was initiated. This is primary useful when debugging. The + scan controls are not showing by default. Pressing the toolbar button will show them along the right side of the + window

-

-

+

 

-

Only the parts of the byte pattern that make up the instructions will be searched for - with the remaining bits used as wildcards. The bit pattern 10000101 11...... 01010... - 01101010 ........ 01011... is searched for where the .'s indicate the wild carded - values.
-
-

-
Option 3: +

Memory Search Window With Scan Controls Showing

+ +

Scan Values Button:

+ +
+

This button initiates a scan of the byte values in all the matches in the results + table. Depending on the selected scan option, the set of matches in the table may be + reduced based on the values that changed.

+
+ +

Scan Option Radio Buttons

+ +
+

One of the following buttons can be selected and they determine how the set of + current matches should be reduced based on changed values.

+ +
    +
  • Equals This option will keep all matches whose values haven't changed and + remove any matches whose bytes have changed.
  • + +
  • Not Equals This option will keep all matches whose values have changed and + will remove any matches whose bytes have not changed.
  • + +
  • Increased This option will keep all matches whose values have increased + and will remove any matches whose values decreased or stayed the same.
  • + +
  • Decreased This option will keep all matches whose values have decreased + and will remove any matches whose values increased or stayed the same.
  • +
+ +

The Increased or + Decreased options really only make sense for matches that represent numerical + values such as integers or floats. In other cases it makes the determination based on + the first byte in the sequence that changed, as if they were a sequence of 1 byte + unsigned values.

+ +

Another way to see changed bytes is + to use the Refresh toolbar action. This will + update the bytes for each search result and show them in red without reducing the set + of results.

+
+
+ +

Results Table

-

If the Include Operands (except constants) option is chosen then the search - will find all instances of the instruction and all operands except the 0x14 which is a - constant.

- -

- -

The bit pattern 10000101 11000000 01010110 01101010 ........ 01011110 which - equates to the byte pattern 85 c0 56 6a xx 5e is searched for where xx can be any - number N between 0x0 and 0xff.
-
-

+

The bottom part of the window is the search results table. Each row in the table + represents one search match. The table can contain combined results from multiple + searches. At the bottom of the results table, all the standard table filters are + available. The table has the following default columns, but additional columns can be + added by right-clicking on any column header.

+ + + +

Options

+ +
+

The options panel is not displayed by default. Pressing the toolbar button will show them along the right side of the + window.

+
+ +

+  

+ +

Memory Search Window With Options Open

+ +

Byte Options

+
+

These are general options that affect most searches.

+
+ +

Decimal Options

+ +
+

These options apply when parsing input as decimal values.

+ + +
+ +

String Options

+ +
+

These options apply when parsing input as string data.

+ + +
+ +

Code Type Filters

+ +
+

These are filters that can be applied to choose what type(s) of code units to + include in the results. By default, they are all selected. The types are:

+ + +
+ +

Memory Regions

+ +
+

Choose one or more memory regions to search. The available regions can vary depending + on the context, but the default regions are:

+ + +
+ + + + + +
+ +

Search Formats

+ +
+

The selected format determines how the user input is used to generate a search byte + sequence (and possibly mask byte sequence). They are also used to format bytes back into + "values" to be displayed in the table, if applicable.

+ +

See the page on Search Formats for full details on each + format.

+
+ +

Actions

+ +
+ + +

  Incremental Search Forward

+ +
+

This action searches forward in memory, starting at the address just after the current + cursor location. It will continue until a match is found or the highest address in the + search space is reached. It does not "wrap". If a match is found, it it is added to the + current table of results.

+
+ +

  Incremental Search Backwards

+ +
+

This action searches backwards in memory, starting at the address just before the + current cursor location. It will continue until a match is found or the lowest address in + the search space is reached. It does not "wrap". If a match is found, it it is added to + the current table of results.

+
+ +

  Refresh

+ +
+

This action will read the bytes again from memory for every match in the results + table, looking to see if any of the bytes have changed. If so, the Match Bytes and + Match Value columns will display the changed values in red.

+
+ +

  Toggle + Search Controls

+ +
+

This action toggles the search controls on or off.

+
+ +

  Toggle Scan + Controls

+ +
+

This action toggles the scan controls on or off.

+
+ +

  Toggle + Options Panel

+ +
+

This action toggles the options display on or off.

+
+ +

  Make Selection

+ +
+

This action will create a selection in the associated view from all the currently + selected match table rows.

+
+ +

  Toggle Single Click + Navigation

+ +
+

This action toggles on or off whether a single row selection change triggers + navigation in the associated view. If this option is off, the user must double-click on a + row to navigate in the associated view.

+
+ +

  Delete Selected + Table Rows

+ +
+

This action deletes all selected rows in the results match table.

+
+
+ +

Combining Searches

+ +
+

Results from multiple searches can be combined in various ways. These options are only + available once you have results in the table. Once results are present, the Search + Button changes to a button that has a drop down menu that allows you do decide how you + want additional searches to interact with the current results showing in the results table. + The options are as follows:

+ +

New Search

+ +
+ This option causes all existing result matches to be replaced with the results of the new + search. When this option is selected, the button text will show "New Search". +

This does not create a new + search memory window, but re-uses this window. To create a new + search window, you must go back to the search memory action from the main menu.

+
+ +

A union B

+ +
+ This option adds the results from the new search to all the existing result matches. When + this option is selected, the button text will show "Add To Search". +
+ +

A intersect B

+ +
+ This option will combine the results of the new search with the existing search, but only + keep results that are in both the existing result set and the new search result set. When + this option is selected, the button text will show "Intersect Search". +
+ +

A xor B

+ +
+ This option will combine the results of the new search with the existing search, but only + keep results that are in either the new or existing results, but not both. + When this option is selected, the button text will show "Xor Search". +
+ +

A - B

+ +
+ Subtracts the new results from the existing results. When + this option is selected, the button text will show "A-B Search". +
+ +

B - A

+ +
+ Subtracts the existing results from the new results. When this option is + selected, the button text will show "B-A Search". +
+ +

Many of these set operations only make + sense if you do advanced searches using wildcards. For example, if you do a search for + integer values of 5, it would make no sense to intersect that with a search for integer + values of 3. The sets are mutually exclusive, so the intersection would be empty. + Explaining how to take advantage of these options is beyond the scope of this document.

+
+ +

Search Forward/Backwards Using Global Actions

+ +
+

Once at least one search has been executed using the Memory Search Window, the search can be repeated in an + incremental fashion outside a search window using global actions in the main tool menu or + their assigned default keybindings.

+ + +

Search Memory Forwards:

+ +

This action will use the input data and settings from the last memory search and begin + searching forwards in memory starting at the cursor location in the associated Listing + display. If a match is found, the cursor in the Listing will be placed on the found match + location. To execute this action, select Search Search Memory Forwards from the main tool menu or press + F3 (the default keybinding.)

+ + +

Search Memory Backwards:

+ +

This action will use the input data and settings from the last memory search and begin + searching backwards in memory starting at the cursor location in the associated Listing + display. If a match is found, the cursor in the Listing will be placed on the found match + location. To execute this action, select Search Search Memory Backwards from the main tool menu or press + <Shift>F3 (the default keybinding.)

+
+ +

Highlight Search Options

+ +
+

You can control how the bytes found in the search be highlighted in the Code Browser by + selecting the Highlight Search Results checkbox on the Search Options panel. To view + the Search Options, select Edit + Tool Options... from the tool menu, then select the Search node in the + Options tree in the Options dialog. You can also change the highlight color. Click on the + color bar next to Highlight Color to bring up a color chooser. Choose the new color, + click on the OK button. Apply your changes by clicking on the OK or + Apply button on the Options dialog. 

+ +
+

Highlights are displayed for the + last search that you did. For example, if you bring up the Search Program Text dialog and + search for text, that string now becomes the new highlight string. Similarly, if you + invoke cursor + text highlighting, that becomes the new highlight string.

+
+ +

Highlights are removed when you close the search window.

+
+
-

NoteThe previous operations can only work on a - single selected region. If multiple regions are selected, the following error dialog - will be shown and the operation will be cancelled.

+

Provided by: Memory Search Plugin

-

-
+

Related Topics:

+ +

- -

Provided by: the MemSearchPlugin  

-   - -

Related Topics:

- - -
-
diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MemorySearchProvider.png b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MemorySearchProvider.png new file mode 100644 index 0000000000..86aa4931eb Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MemorySearchProvider.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MemorySearchProviderWithOptionsOn.png b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MemorySearchProviderWithOptionsOn.png new file mode 100644 index 0000000000..87bf8867af Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MemorySearchProviderWithOptionsOn.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MemorySearchProviderWithScanPanelOn.png b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MemorySearchProviderWithScanPanelOn.png new file mode 100644 index 0000000000..94a13ce3ba Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MemorySearchProviderWithScanPanelOn.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MultipleSelectionError.png b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MultipleSelectionError.png index daf1fa2a52..8d48fee05a 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MultipleSelectionError.png and b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/MultipleSelectionError.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryBinary.png b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryBinary.png deleted file mode 100644 index def1ebe504..0000000000 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryBinary.png and /dev/null differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryDecimal.png b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryDecimal.png deleted file mode 100644 index 42fcf5ee7a..0000000000 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryDecimal.png and /dev/null differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryHex.png b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryHex.png deleted file mode 100644 index 5ff166e5a2..0000000000 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryHex.png and /dev/null differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryRegex.png b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryRegex.png index 34ae7c69f2..0b77e07997 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryRegex.png and b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryRegex.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryString.png b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryString.png deleted file mode 100644 index dea2d8a8fe..0000000000 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchMemoryString.png and /dev/null differ diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/nav/Navigatable.java b/Ghidra/Features/Base/src/main/java/ghidra/app/nav/Navigatable.java index a731f0bcca..443c77a9b4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/nav/Navigatable.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/nav/Navigatable.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. @@ -18,6 +18,8 @@ package ghidra.app.nav; import javax.swing.Icon; import ghidra.app.util.ListingHighlightProvider; +import ghidra.features.base.memsearch.bytesource.AddressableByteSource; +import ghidra.features.base.memsearch.bytesource.ProgramByteSource; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; import ghidra.program.util.ProgramSelection; @@ -193,5 +195,16 @@ public interface Navigatable { * @param highlightProvider the provider * @param program the program */ - public void removeHighlightProvider(ListingHighlightProvider highlightProvider, Program program); + public void removeHighlightProvider(ListingHighlightProvider highlightProvider, + Program program); + + /** + * Returns a source for providing byte values of the program associated with this + * navigatable. For a static program, this is just a wrapper for a program's memory. But + * dynamic programs require special handling for reading bytes. + * @return a source of bytes for the navigatable's program + */ + public default AddressableByteSource getByteSource() { + return new ProgramByteSource(getProgram()); + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/MemSearchDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/MemSearchDialog.java index 58aafb4a0c..061e2e1598 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/MemSearchDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/MemSearchDialog.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. @@ -305,6 +305,7 @@ class MemSearchDialog extends ReusableDialogComponentProvider { JPanel inputPanel = new JPanel(); inputPanel.setLayout(new GridLayout(0, 1)); valueComboBox = new GhidraComboBox<>(); + valueComboBox.setAutoCompleteEnabled(false); // we do our own completion with validation valueComboBox.setEditable(true); valueComboBox.setToolTipText(currentFormat.getToolTip()); valueComboBox.setDocument(new RestrictedInputDocument()); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/MemSearchPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/MemSearchPlugin.java index 86911f59b8..493b2689b6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/MemSearchPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/MemSearchPlugin.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. @@ -16,14 +16,14 @@ package ghidra.app.plugin.core.searchmem; import java.awt.Color; -import java.awt.event.KeyEvent; import java.util.*; import javax.swing.Icon; import javax.swing.JComponent; import docking.*; -import docking.action.*; +import docking.action.DockingAction; +import docking.action.MenuData; import docking.tool.ToolConstants; import docking.widgets.fieldpanel.support.Highlight; import docking.widgets.table.threaded.*; @@ -64,7 +64,7 @@ import ghidra.util.task.*; */ //@formatter:off @PluginInfo( - status = PluginStatus.RELEASED, + status = PluginStatus.DEPRECATED, packageName = CorePluginPackage.NAME, category = PluginCategoryNames.SEARCH, shortDescription = "Search bytes in memory", @@ -73,12 +73,12 @@ import ghidra.util.task.*; " The value may contain \"wildcards\" or regular expressions" + " that will match any byte or nibble.", servicesRequired = { ProgramManager.class, GoToService.class, TableService.class, CodeViewerService.class }, - servicesProvided = { MemorySearchService.class }, +// servicesProvided = { MemorySearchService.class }, eventsConsumed = { ProgramSelectionPluginEvent.class } ) //@formatter:on public class MemSearchPlugin extends Plugin implements OptionsChangeListener, - DockingContextListener, NavigatableRemovalListener, MemorySearchService { + DockingContextListener, NavigatableRemovalListener { /** Constant for read/writeConfig() for dialog options */ private static final String SHOW_ADVANCED_OPTIONS = "Show Advanced Options"; @@ -243,12 +243,12 @@ public class MemSearchPlugin extends Plugin implements OptionsChangeListener, } - @Override - public void setIsMnemonic(boolean isMnemonic) { - // provides the dialog with the knowledge of whether or not - // the action being performed is a MnemonicSearchPlugin - this.isMnemonic = isMnemonic; - } +// @Override +// public void setIsMnemonic(boolean isMnemonic) { +// // provides the dialog with the knowledge of whether or not +// // the action being performed is a MnemonicSearchPlugin +// this.isMnemonic = isMnemonic; +// } private void setNavigatable(Navigatable newNavigatable) { if (newNavigatable == navigatable) { @@ -329,16 +329,16 @@ public class MemSearchPlugin extends Plugin implements OptionsChangeListener, return new BytesFieldLocation(program, address); } - @Override - public void search(byte[] bytes, NavigatableActionContext context) { - setNavigatable(context.getNavigatable()); - invokeSearchDialog(context); - } - - @Override - public void setSearchText(String maskedString) { - searchDialog.setSearchText(maskedString); - } +// @Override +// public void search(byte[] bytes, NavigatableActionContext context) { +// setNavigatable(context.getNavigatable()); +// invokeSearchDialog(context); +// } +// +// @Override +// public void setSearchText(String maskedString) { +// searchDialog.setSearchText(maskedString); +// } private void createActions() { searchAction = new NavigatableContextAction("Search Memory", getName(), false) { @@ -349,9 +349,8 @@ public class MemSearchPlugin extends Plugin implements OptionsChangeListener, } }; searchAction.setHelpLocation(new HelpLocation(HelpTopics.SEARCH, searchAction.getName())); - String[] menuPath = new String[] { "&Search", "&Memory..." }; + String[] menuPath = new String[] { "&Search", "Memory (Deprecated)..." }; searchAction.setMenuBarData(new MenuData(menuPath, "search")); - searchAction.setKeyBindingData(new KeyBindingData('S', 0)); searchAction.setDescription("Search Memory for byte sequence"); searchAction.addToWindowWhen(NavigatableActionContext.class); tool.addAction(searchAction); @@ -372,10 +371,10 @@ public class MemSearchPlugin extends Plugin implements OptionsChangeListener, .setHelpLocation(new HelpLocation(HelpTopics.SEARCH, searchAgainAction.getName())); menuPath = new String[] { "&Search", "Repeat Memory Search" }; searchAgainAction.setMenuBarData(new MenuData(menuPath, "search")); - searchAgainAction.setKeyBindingData(new KeyBindingData(KeyEvent.VK_F3, 0)); searchAgainAction.setDescription("Search Memory for byte sequence"); searchAgainAction.addToWindowWhen(NavigatableActionContext.class); tool.addAction(searchAgainAction); + } private void initializeOptionListeners() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/MemorySearchService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/MemorySearchService.java index 379fa84118..d52c9e6c60 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/MemorySearchService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/MemorySearchService.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. @@ -15,23 +15,36 @@ */ package ghidra.app.services; -import ghidra.app.context.NavigatableActionContext; +import ghidra.app.nav.Navigatable; +import ghidra.features.base.memsearch.gui.MemorySearchProvider; +import ghidra.features.base.memsearch.gui.SearchSettings; +/** + * Service for invoking the {@link MemorySearchProvider} + * @deprecated This is not a generally useful service, may go away at some point + */ +@Deprecated(since = "11.2") public interface MemorySearchService { - /* - * sets up MemSearchDialog based on given bytes + /** + * Creates a new memory search provider window + * @param navigatable the navigatable used to get bytes to search + * @param input the input string to search for + * @param settings the settings that determine how to interpret the input string + * @param useSelection true if the provider should automatically restrict to a selection if + * a selection exists in the navigatable */ - public void search(byte[] bytes, NavigatableActionContext context); + public void createMemorySearchProvider(Navigatable navigatable, String input, + SearchSettings settings, boolean useSelection); - /* - * sets the search value field to the masked bit string - */ - public void setSearchText(String maskedString); +// These method were removed because they didn't work correctly and were specific to the needs of +// one outlier plugin. The functionality has been replaced by the above method, which is also +// unlikely to be useful. - /* - * determines whether the dialog was called by a mnemonic or not - */ - public void setIsMnemonic(boolean isMnemonic); +// public void search(byte[] bytes, NavigatableActionContext context); +// +// public void setSearchText(String maskedString); +// +// public void setIsMnemonic(boolean isMnemonic); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ArrayValuesFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ArrayValuesFieldFactory.java index a467b26514..c8ec329427 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ArrayValuesFieldFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ArrayValuesFieldFactory.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. @@ -25,8 +25,10 @@ import ghidra.app.util.viewer.format.FieldFormatModel; import ghidra.app.util.viewer.format.FormatManager; import ghidra.app.util.viewer.proxy.ProxyObj; import ghidra.framework.options.*; +import ghidra.program.model.address.Address; import ghidra.program.model.data.DataType; import ghidra.program.model.listing.*; +import ghidra.program.util.AddressFieldLocation; import ghidra.program.util.ProgramLocation; import ghidra.util.HelpLocation; import ghidra.util.exception.AssertException; @@ -128,16 +130,34 @@ public class ArrayValuesFieldFactory extends FieldFactory { @Override public FieldLocation getFieldLocation(ListingField lf, BigInteger index, int fieldNum, ProgramLocation location) { - if (!(location instanceof ArrayElementFieldLocation)) { + + // Unless the location is specifically targeting the address field, then we should + // process the location because the arrays display is different from all other format + // models in that one line actually represents more than one array data element. So + // this field has the best chance of representing the location for array data elements. + + if (location instanceof AddressFieldLocation) { return null; } - ArrayElementFieldLocation loc = (ArrayElementFieldLocation) location; - ListingTextField btf = (ListingTextField) lf; - Data firstDataOnLine = (Data) btf.getProxy().getObject(); - int elementIndex = loc.getElementIndexOnLine(firstDataOnLine); - RowColLocation rcl = btf.dataToScreenLocation(elementIndex, loc.getCharOffset()); - return new FieldLocation(index, fieldNum, rcl.row(), rcl.col()); + RowColLocation rcl = getDataRowColumnLocation(location, lf); + return new FieldLocation(index, fieldNum, rcl.row(), rcl.col()); + } + + private RowColLocation getDataRowColumnLocation(ProgramLocation location, ListingField field) { + ListingTextField ltf = (ListingTextField) field; + Data firstDataOnLine = (Data) field.getProxy().getObject(); + + if (location instanceof ArrayElementFieldLocation loc) { + int elementIndex = loc.getElementIndexOnLine(firstDataOnLine); + return ltf.dataToScreenLocation(elementIndex, loc.getCharOffset()); + } + + Address byteAddress = location.getByteAddress(); + int byteOffset = (int) byteAddress.subtract(firstDataOnLine.getAddress()); + int componentSize = firstDataOnLine.getLength(); + int elementOffset = byteOffset / componentSize; + return ltf.dataToScreenLocation(elementOffset, 0); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesequence/AddressableByteSequence.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesequence/AddressableByteSequence.java new file mode 100644 index 0000000000..28987810eb --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesequence/AddressableByteSequence.java @@ -0,0 +1,125 @@ +/* ### + * 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.features.base.memsearch.bytesequence; + +import ghidra.features.base.memsearch.bytesource.AddressableByteSource; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; + +/** + * This class provides a {@link ByteSequence} view into an {@link AddressableByteSource}. By + * specifying an address and length, this class provides a view into the byte source + * as a indexable sequence of bytes. It is mutable and can be reused by setting a new + * address range for this sequence. This was to avoid constantly allocating large byte arrays. + */ +public class AddressableByteSequence implements ByteSequence { + + private final AddressableByteSource byteSource; + private final byte[] bytes; + private final int capacity; + + private Address startAddress; + private int length; + + /** + * Constructor + * @param byteSource the source of the underlying bytes that is a buffer into + * @param capacity the maximum size range that this object will buffer + */ + public AddressableByteSequence(AddressableByteSource byteSource, int capacity) { + this.byteSource = byteSource; + this.capacity = capacity; + this.length = 0; + this.bytes = new byte[capacity]; + } + + /** + * Sets this view to an empty byte sequence + */ + public void clear() { + startAddress = null; + length = 0; + } + + /** + * Sets the range of bytes that this object will buffer. This immediately will read the bytes + * from the byte source into it's internal byte array buffer. + * @param range the range of bytes to buffer + */ + public void setRange(AddressRange range) { + // Note that this will throw an exception if the range length is larger then Integer.MAX + // which is unsupported by the ByteSequence interface + try { + setRange(range.getMinAddress(), range.getBigLength().intValueExact()); + } + catch (ArithmeticException e) { + throw new IllegalArgumentException("Length exceeds capacity"); + } + } + + /** + * Returns the address of the byte represented by the given index into this buffer. + * @param index the index into the buffer to get its associated address + * @return the Address for the given index + */ + public Address getAddress(int index) { + if (index < 0 || index >= length) { + throw new IndexOutOfBoundsException(); + } + if (index == 0) { + return startAddress; + } + return startAddress.add(index); + } + + @Override + public int getLength() { + return length; + } + + @Override + public byte getByte(int index) { + if (index < 0 || index >= length) { + throw new IndexOutOfBoundsException(); + } + return bytes[index]; + } + + @Override + public byte[] getBytes(int index, int size) { + if (index < 0 || index + size > length) { + throw new IndexOutOfBoundsException(); + } + byte[] results = new byte[size]; + System.arraycopy(bytes, index, results, 0, size); + return results; + } + + @Override + public boolean hasAvailableBytes(int index, int length) { + return index >= 0 && index + length <= getLength(); + } + + private void setRange(Address start, int length) { + if (length > capacity) { + throw new IllegalArgumentException("Length exceeds capacity"); + } + this.startAddress = start; + this.length = length; + byteSource.getBytes(start, bytes, length); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesequence/ByteSequence.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesequence/ByteSequence.java new file mode 100644 index 0000000000..a4818d754a --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesequence/ByteSequence.java @@ -0,0 +1,53 @@ +/* ### + * 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.features.base.memsearch.bytesequence; + +/** + * An interface for accessing bytes from a byte source. + */ +public interface ByteSequence { + + /** + * Returns the length of available bytes. + * @return the length of the sequence of bytes + */ + public int getLength(); + + /** + * Returns the byte at the given index. The index must between 0 and the extended length. + * @param index the index in the byte sequence to retrieve a byte value + * @return the byte at the given index + */ + public byte getByte(int index); + + /** + * A convenience method for checking if this sequence can provide a range of bytes from some + * offset. + * @param index the index of the start of the range to check for available bytes + * @param length the length of the range to check for available bytes + * @return true if bytes are available for the given range + */ + public boolean hasAvailableBytes(int index, int length); + + /** + * Returns a byte array containing the bytes from the given range. + * @param start the start index of the range to get bytes + * @param length the number of bytes to get + * @return a byte array containing the bytes from the given range + */ + public byte[] getBytes(int start, int length); + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesequence/ExtendedByteSequence.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesequence/ExtendedByteSequence.java new file mode 100644 index 0000000000..46d324751e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesequence/ExtendedByteSequence.java @@ -0,0 +1,98 @@ +/* ### + * 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.features.base.memsearch.bytesequence; + +/** + * A class for accessing a contiguous sequence of bytes from some underlying byte source to + * be used for searching for a byte pattern within the byte source. This sequence of bytes + * consists of two parts; the primary sequence and an extended sequence. Search matches + * must begin in the primary sequence, but may extend into the extended sequence. + *

+ * Searching large ranges of memory can be partitioned into searching smaller chunks. But + * to handle search sequences that span chunks, two chunks are presented at a time, with the second + * chunk being the extended bytes. On the next iteration of the search loop, the extended chunk + * will become the primary chunk, with the next chunk after that becoming the extended sequence + * and so on. + */ +public class ExtendedByteSequence implements ByteSequence { + + private ByteSequence main; + private ByteSequence extended; + private int extendedLength; + + /** + * Constructs an extended byte sequence from two {@link ByteSequence}s. + * @param main the byte sequence where search matches may start + * @param extended the byte sequence where search matches may extend into + * @param extendedLimit specifies how much of the extended byte sequence to allow search + * matches to extend into. (The extended buffer will be the primary buffer next time, so + * it is a full size buffer, but we only need to use a portion of it to support overlap. + */ + public ExtendedByteSequence(ByteSequence main, ByteSequence extended, int extendedLimit) { + this.main = main; + this.extended = extended; + this.extendedLength = main.getLength() + Math.min(extendedLimit, extended.getLength()); + } + + @Override + public int getLength() { + return main.getLength(); + } + + /** + * Returns the overall length of sequence of available bytes. This will be the length of + * the primary sequence as returned by {@link #getLength()} plus the length of the available + * extended bytes, if any. + * @return the + */ + public int getExtendedLength() { + return extendedLength; + } + + @Override + public byte getByte(int i) { + int mainLength = main.getLength(); + if (i >= mainLength) { + return extended.getByte(i - mainLength); + } + return main.getByte(i); + } + + @Override + public byte[] getBytes(int index, int size) { + if (index < 0 || index + size > extendedLength) { + throw new IndexOutOfBoundsException(); + } + int length = main.getLength(); + if (index + size < length) { + return main.getBytes(index, size); + } + if (index >= length) { + return extended.getBytes(index - length, size); + } + // otherwise it spans + byte[] results = new byte[size]; + for (int i = 0; i < size; i++) { + results[i] = getByte(index + i); + } + return results; + } + + @Override + public boolean hasAvailableBytes(int index, int length) { + return index >= 0 && index + length <= getExtendedLength(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/AddressableByteSource.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/AddressableByteSource.java new file mode 100644 index 0000000000..9bbb081eed --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/AddressableByteSource.java @@ -0,0 +1,59 @@ +/* ### + * 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.features.base.memsearch.bytesource; + +import java.util.List; + +import ghidra.program.model.address.Address; + +/** + * Interface for reading bytes from a program. This provides a level of indirection for reading the + * bytes of a program so that the provider of the bytes can possibly do more than just reading the + * bytes from the static program. For example, a debugger would have the opportunity to refresh the + * bytes first. + *

+ * This interface also provides methods for determining what regions of memory can be queried and + * what addresses sets are associated with those regions. This would allow client to present choices + * about what areas of memory they are interested in AND are valid to be examined. + */ +public interface AddressableByteSource { + + /** + * Retrieves the byte values for an address range. + * + * @param address The address of the first byte in the range + * @param bytes the byte array to store the retrieved byte values + * @param length the number of bytes to retrieve + * @return the number of bytes actually retrieved + */ + public int getBytes(Address address, byte[] bytes, int length); + + /** + * Returns a list of memory regions where each region has an associated address set of valid + * addresses that can be read. + * + * @return a list of readable regions + */ + public List getSearchableRegions(); + + /** + * Invalidates any caching of byte values. This intended to provide a hint in debugging scenario + * that we are about to issue a sequence of byte value requests where we are re-acquiring + * previous requested byte values to look for changes. + */ + public void invalidate(); + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/EmptyByteSource.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/EmptyByteSource.java new file mode 100644 index 0000000000..a9e2f1ff33 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/EmptyByteSource.java @@ -0,0 +1,42 @@ +/* ### + * 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.features.base.memsearch.bytesource; + +import java.util.List; + +import ghidra.program.model.address.Address; + +/** + * Implementation for an empty {@link AddressableByteSource} + */ +public enum EmptyByteSource implements AddressableByteSource { + INSTANCE; + + @Override + public int getBytes(Address address, byte[] bytes, int length) { + return 0; + } + + @Override + public List getSearchableRegions() { + return List.of(); + } + + @Override + public void invalidate() { + // nothing to do + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/ProgramByteSource.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/ProgramByteSource.java new file mode 100644 index 0000000000..0506245e7b --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/ProgramByteSource.java @@ -0,0 +1,56 @@ +/* ### + * 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.features.base.memsearch.bytesource; + +import java.util.List; + +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.Memory; +import ghidra.program.model.mem.MemoryAccessException; + +/** + * {@link AddressableByteSource} implementation for a Ghidra {@link Program} + */ +public class ProgramByteSource implements AddressableByteSource { + + private Memory memory; + + public ProgramByteSource(Program program) { + memory = program.getMemory(); + } + + @Override + public int getBytes(Address address, byte[] bytes, int length) { + try { + return memory.getBytes(address, bytes, 0, length); + } + catch (MemoryAccessException e) { + return 0; + } + } + + @Override + public List getSearchableRegions() { + return ProgramSearchRegion.ALL; + } + + @Override + public void invalidate() { + // nothing to do in the static case + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/ProgramSearchRegion.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/ProgramSearchRegion.java new file mode 100644 index 0000000000..c2bbc3fd2f --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/ProgramSearchRegion.java @@ -0,0 +1,78 @@ +/* ### + * 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.features.base.memsearch.bytesource; + +import java.util.List; + +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.Memory; + +/** + * An enum specifying the selectable regions within a {@link Program} that users can select for + * searching memory. + */ +public enum ProgramSearchRegion implements SearchRegion { + LOADED("Loaded Blocks", + "Searches all memory blocks that represent loaded program instructions and data") { + + @Override + public boolean isDefault() { + return true; + } + + @Override + public AddressSetView getAddresses(Program program) { + Memory memory = program.getMemory(); + return memory.getLoadedAndInitializedAddressSet(); + } + }, + OTHER("All Other Blocks", "Searches non-loaded initialized blocks") { + + @Override + public boolean isDefault() { + return false; + } + + @Override + public AddressSetView getAddresses(Program program) { + Memory memory = program.getMemory(); + AddressSetView all = memory.getAllInitializedAddressSet(); + AddressSetView loaded = memory.getLoadedAndInitializedAddressSet(); + return all.subtract(loaded); + } + }; + + public static final List ALL = List.of(values()); + + private String name; + private String description; + + ProgramSearchRegion(String name, String description) { + this.name = name; + this.description = description; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return description; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/SearchRegion.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/SearchRegion.java new file mode 100644 index 0000000000..e6ca49a812 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/bytesource/SearchRegion.java @@ -0,0 +1,52 @@ +/* ### + * 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.features.base.memsearch.bytesource; + +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.listing.Program; + +/** + * Interface to specify a named region within a byte source (Program) that users can select to + * specify {@link AddressSetView}s that can be searched. + */ +public interface SearchRegion { + + /** + * The name of the region. + * @return the name of the region + */ + public String getName(); + + /** + * Returns a description of the region. + * @return a description of the region + */ + public String getDescription(); + + /** + * Returns the set of addresses from a specific program that is associated with this region. + * @param program the program that determines the specific addresses for a named region + * @return the set of addresses for this region as applied to the given program + */ + public AddressSetView getAddresses(Program program); + + /** + * Returns true if this region should be included in the default selection of which regions to + * search. + * @return true if this region should be selected by default + */ + public boolean isDefault(); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/combiner/Combiner.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/combiner/Combiner.java new file mode 100644 index 0000000000..934a6007f2 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/combiner/Combiner.java @@ -0,0 +1,123 @@ +/* ### + * 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.features.base.memsearch.combiner; + +import java.util.*; +import java.util.function.BiFunction; + +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.program.model.address.Address; + +/** + * An enum of search results "combiners". Each combiner determines how to combine two sets of + * memory search results. The current or existing results is represented as the "A" set and the + * new search is represented as the "B" set. + */ +public enum Combiner { + REPLACE("New", Combiner::replace), + UNION("Add To", Combiner::union), + INTERSECT("Intersect", Combiner::intersect), + XOR("Xor", Combiner::xor), + A_MINUS_B("A-B", Combiner::subtract), + B_MINUS_A("B-A", Combiner::reverseSubtract); + + private String name; + private BiFunction, List, Collection> function; + + private Combiner(String name, + BiFunction, List, Collection> function) { + this.name = name; + this.function = function; + } + + public String getName() { + return name; + } + + public Collection combine(List matches1, List matches2) { + return function.apply(matches1, matches2); + } + + private static Collection replace(List matches1, + List matches2) { + + return matches2; + } + + private static Collection union(List matches1, + List matches2) { + + Map matches1Map = createMap(matches1); + for (MemoryMatch match2 : matches2) { + Address address = match2.getAddress(); + MemoryMatch match1 = matches1Map.get(address); + if (match1 == null || match2.getLength() > match1.getLength()) { + matches1Map.put(address, match2); + } + } + return matches1Map.values(); + } + + private static Collection intersect(List matches1, + List matches2) { + + List intersection = new ArrayList<>(); + Map matches1Map = createMap(matches1); + + for (MemoryMatch match2 : matches2) { + Address address = match2.getAddress(); + MemoryMatch match1 = matches1Map.get(address); + if (match1 != null) { + MemoryMatch best = match2.getLength() > match1.getLength() ? match2 : match1; + intersection.add(best); + } + } + return intersection; + } + + private static List xor(List matches1, List matches2) { + List results = new ArrayList<>(); + results.addAll(subtract(matches1, matches2)); + results.addAll(subtract(matches2, matches1)); + return results; + } + + private static Collection subtract(List matches1, + List matches2) { + + Map matches1Map = createMap(matches1); + + for (MemoryMatch match2 : matches2) { + Address address = match2.getAddress(); + matches1Map.remove(address); + } + return matches1Map.values(); + } + + private static Collection reverseSubtract(List matches1, + List matches2) { + return subtract(matches2, matches1); + } + + private static Map createMap(List matches) { + Map map = new HashMap<>(); + for (MemoryMatch result : matches) { + map.put(result.getAddress(), result); + } + return map; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/BinarySearchFormat.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/BinarySearchFormat.java new file mode 100644 index 0000000000..c1d425dcd3 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/BinarySearchFormat.java @@ -0,0 +1,188 @@ +/* ### + * 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.features.base.memsearch.format; + +import java.util.*; + +import ghidra.features.base.memsearch.gui.SearchSettings; +import ghidra.features.base.memsearch.matcher.*; +import ghidra.util.HTMLUtilities; + +/** + * {@link SearchFormat} for parsing and display bytes in a binary format. This format only + * accepts 0s or 1s or wild card characters. + */ +class BinarySearchFormat extends SearchFormat { + private static final String VALID_CHARS = "01x?."; + private static final int MAX_GROUP_SIZE = 8; + + BinarySearchFormat() { + super("Binary"); + } + + @Override + public ByteMatcher parse(String input, SearchSettings settings) { + input = input.trim(); + if (input.isBlank()) { + return new InvalidByteMatcher(""); + } + + List byteGroups = getByteGroups(input); + + if (hasInvalidChars(byteGroups)) { + return new InvalidByteMatcher("Invalid character"); + } + + if (checkGroupSize(byteGroups)) { + return new InvalidByteMatcher("Max group size exceeded. Enter to add more."); + } + + byte[] bytes = getBytes(byteGroups); + byte[] masks = getMask(byteGroups); + return new MaskedByteSequenceByteMatcher(input, bytes, masks, settings); + } + + @Override + public String getToolTip() { + return HTMLUtilities.toHTML( + "Interpret value as a sequence of binary digits.\n" + + "Spaces will start the next byte. Bit sequences less\n" + + "than 8 bits are padded with 0's to the left. \n" + + "Enter 'x', '.' or '?' for a wildcard bit"); + } + + private boolean checkGroupSize(List byteGroups) { + for (String byteGroup : byteGroups) { + if (byteGroup.length() > MAX_GROUP_SIZE) { + return true; + } + } + return false; + } + + private List getByteGroups(String input) { + List list = new ArrayList(); + StringTokenizer st = new StringTokenizer(input); + while (st.hasMoreTokens()) { + list.add(st.nextToken()); + } + return list; + } + + private boolean hasInvalidChars(List byteGroups) { + for (String byteGroup : byteGroups) { + if (hasInvalidChars(byteGroup)) { + return true; + } + } + return false; + } + + private boolean hasInvalidChars(String string) { + for (int i = 0; i < string.length(); i++) { + if (VALID_CHARS.indexOf(string.charAt(i)) < 0) { + return true; + } + } + return false; + } + + private byte getByte(String token) { + byte b = 0; + for (int i = 0; i < token.length(); i++) { + b <<= 1; + char c = token.charAt(i); + if (c == '1') { + b |= 1; + } + } + return b; + } + + /** + * Return a mask byte that has a bit set to 1 for each bit that is not a wildcard. Any bits + * that aren't specified (i.e. token.lenght < 8) are treated as valid test bits. + * @param token the string of bits to determine a mask for. + */ + private byte getMask(String token) { + byte b = 0; + for (int i = 0; i < 8; i++) { + b <<= 1; + if (i < token.length()) { + char c = token.charAt(i); + if (c == '1' || c == '0') { + b |= 1; + } + } + else { + b |= 1; + } + + } + + return b; + } + + private byte[] getBytes(List byteGroups) { + byte[] bytes = new byte[byteGroups.size()]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = getByte(byteGroups.get(i)); + } + return bytes; + } + + private byte[] getMask(List byteGroups) { + byte[] masks = new byte[byteGroups.size()]; + for (int i = 0; i < masks.length; i++) { + masks[i] = getMask(byteGroups.get(i)); + } + return masks; + } + + @Override + public String convertText(String text, SearchSettings oldSettings, SearchSettings newSettings) { + SearchFormat oldFormat = oldSettings.getSearchFormat(); + if (oldFormat.getFormatType() != SearchFormatType.STRING_TYPE) { + ByteMatcher byteMatcher = oldFormat.parse(text, oldSettings); + if ((byteMatcher instanceof MaskedByteSequenceByteMatcher matcher)) { + byte[] bytes = matcher.getBytes(); + byte[] mask = matcher.getMask(); + return getMaskedInputString(bytes, mask); + } + } + + return isValidText(text, newSettings) ? text : ""; + } + + private String getMaskedInputString(byte[] bytes, byte[] masks) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + for (int shift = 7; shift >= 0; shift--) { + int bit = bytes[i] >> shift & 0x1; + int maskBit = masks[i] >> shift & 0x1; + builder.append(maskBit == 0 ? '.' : Integer.toString(bit)); + } + builder.append(" "); + } + + return builder.toString().trim(); + } + + @Override + public SearchFormatType getFormatType() { + return SearchFormatType.BYTE; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/DecimalSearchFormat.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/DecimalSearchFormat.java new file mode 100644 index 0000000000..ff5ae7d709 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/DecimalSearchFormat.java @@ -0,0 +1,280 @@ +/* ### + * 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.features.base.memsearch.format; + +import java.math.BigInteger; +import java.util.StringTokenizer; + +import org.bouncycastle.util.Arrays; + +import ghidra.features.base.memsearch.gui.SearchSettings; +import ghidra.features.base.memsearch.matcher.*; +import ghidra.util.HTMLUtilities; + +/** + * {@link SearchFormat} for parsing and display bytes in a decimal format. It supports sizes of + * 2,4,8,16 and can be either signed or unsigned. + */ +class DecimalSearchFormat extends SearchFormat { + + DecimalSearchFormat() { + super("Decimal"); + } + + @Override + public ByteMatcher parse(String input, SearchSettings settings) { + input = input.trim(); + if (input.isBlank()) { + return new InvalidByteMatcher(""); + } + int byteSize = settings.getDecimalByteSize(); + StringTokenizer tokenizer = new StringTokenizer(input); + int tokenCount = tokenizer.countTokens(); + byte[] bytes = new byte[tokenCount * byteSize]; + int bytesPosition = 0; + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + NumberParseResult result = parseNumber(token, settings); + if (result.errorMessage() != null) { + return new InvalidByteMatcher(result.errorMessage(), result.validInput()); + } + System.arraycopy(result.bytes(), 0, bytes, bytesPosition, byteSize); + bytesPosition += byteSize; + } + return new MaskedByteSequenceByteMatcher(input, bytes, settings); + } + + private NumberParseResult parseNumber(String tok, SearchSettings settings) { + BigInteger min = getMin(settings); + BigInteger max = getMax(settings); + try { + if (tok.equals("-")) { + if (settings.isDecimalUnsigned()) { + return new NumberParseResult(null, + "Negative numbers not allowed for unsigned values", false); + } + return new NumberParseResult(null, "Incomplete negative number", true); + } + BigInteger value = new BigInteger(tok); + if (value.compareTo(min) < 0 || value.compareTo(max) > 0) { + return new NumberParseResult(null, + "Number must be in the range [" + min + ", " + max + "]", false); + } + long longValue = value.longValue(); + return createBytesResult(longValue, settings); + } + catch (NumberFormatException e) { + return new NumberParseResult(null, "Number parse error: " + e.getMessage(), false); + } + } + + private BigInteger getMax(SearchSettings settings) { + boolean unsigned = settings.isDecimalUnsigned(); + int size = settings.getDecimalByteSize(); + int shift = unsigned ? 8 * size : 8 * size - 1; + return BigInteger.ONE.shiftLeft(shift).subtract(BigInteger.ONE); + } + + private BigInteger getMin(SearchSettings settings) { + boolean unsigned = settings.isDecimalUnsigned(); + int size = settings.getDecimalByteSize(); + if (unsigned) { + return BigInteger.ZERO; + } + return BigInteger.ONE.shiftLeft(8 * size - 1).negate(); + } + + private NumberParseResult createBytesResult(long value, SearchSettings settings) { + int byteSize = settings.getDecimalByteSize(); + byte[] bytes = new byte[byteSize]; + for (int i = 0; i < byteSize; i++) { + byte b = (byte) value; + bytes[i] = b; + value >>= 8; + } + if (settings.isBigEndian()) { + reverse(bytes); + } + return new NumberParseResult(bytes, null, true); + } + + @Override + public String getToolTip() { + return HTMLUtilities.toHTML( + "Interpret values as a sequence of decimal numbers, separated by spaces"); + } + + @Override + public int compareValues(byte[] bytes1, byte[] bytes2, SearchSettings settings) { + int byteSize = settings.getDecimalByteSize(); + // check each value one at a time, and return the first one different + for (int i = 0; i < bytes1.length / byteSize; i++) { + long value1 = getValue(bytes1, i * byteSize, settings); + long value2 = getValue(bytes2, i * byteSize, settings); + if (value1 != value2) { + if (byteSize == 8 && settings.isDecimalUnsigned()) { + return Long.compareUnsigned(value1, value2); + } + return Long.compare(value1, value2); + } + } + return 0; + } + + public long getValue(byte[] bytes, int index, SearchSettings settings) { + boolean isBigEndian = settings.isBigEndian(); + int byteSize = settings.getDecimalByteSize(); + boolean isUnsigned = settings.isDecimalUnsigned(); + + byte[] bigEndianBytes = getBigEndianBytes(bytes, index, isBigEndian, byteSize); + long value = isUnsigned ? bigEndianBytes[0] & 0xff : bigEndianBytes[0]; + for (int i = 1; i < byteSize; i++) { + value = (value << 8) | (bigEndianBytes[i] & 0xff); + } + return value; + } + + private byte[] getBigEndianBytes(byte[] bytes, int index, boolean isBigEndian, int byteSize) { + byte[] bigEndianBytes = new byte[byteSize]; + System.arraycopy(bytes, index * byteSize, bigEndianBytes, 0, byteSize); + if (!isBigEndian) { + reverse(bigEndianBytes); + } + return bigEndianBytes; + } + + @Override + public String getValueString(byte[] bytes, SearchSettings settings) { + return getValueString(bytes, settings, false); + } + + protected String getValueString(byte[] bytes, SearchSettings settings, boolean padNegative) { + int byteSize = settings.getDecimalByteSize(); + boolean isBigEndian = settings.isBigEndian(); + boolean isUnsigned = settings.isDecimalUnsigned(); + StringBuilder buffer = new StringBuilder(); + int numValues = bytes.length / byteSize; + for (int i = 0; i < numValues; i++) { + long value = getValue(bytes, i, settings); + String text = isUnsigned ? Long.toUnsignedString(value) : Long.toString(value); + buffer.append(text); + if (i != numValues - 1) { + buffer.append(", "); + } + } + int remainder = bytes.length - numValues * byteSize; + if (remainder > 0) { + byte[] remainderBytes = new byte[remainder]; + System.arraycopy(bytes, numValues * byteSize, remainderBytes, 0, remainder); + byte[] padded = padToByteSize(remainderBytes, byteSize, isBigEndian, padNegative); + long value = getValue(padded, 0, settings); + String text = isUnsigned ? Long.toUnsignedString(value) : Long.toString(value); + if (!buffer.isEmpty()) { + buffer.append(", "); + } + buffer.append(text); + } + + return buffer.toString(); + } + + @Override + public String convertText(String text, SearchSettings oldSettings, SearchSettings newSettings) { + SearchFormat oldFormat = oldSettings.getSearchFormat(); + switch (oldFormat.getFormatType()) { + case BYTE: + return getTextFromBytes(text, oldSettings, newSettings); + case INTEGER: + return convertFromDifferentNumberFormat(text, oldSettings, newSettings); + + case STRING_TYPE: + case FLOATING_POINT: + default: + return isValidText(text, newSettings) ? text : ""; + + } + } + + private String convertFromDifferentNumberFormat(String text, SearchSettings oldSettings, + SearchSettings newSettings) { + int oldSize = oldSettings.getDecimalByteSize(); + int newSize = newSettings.getDecimalByteSize(); + boolean oldUnsigned = oldSettings.isDecimalUnsigned(); + boolean newUnsigned = newSettings.isDecimalUnsigned(); + + if (oldSize == newSize && oldUnsigned == newUnsigned) { + return text; + } + // if the new format is smaller, first try re-parsing to avoid unnecessary 0's + if (oldSize > newSize) { + if (isValidText(text, newSettings)) { + return text; + } + } + return getTextFromBytes(text, oldSettings, newSettings); + } + + private String getTextFromBytes(String text, SearchSettings oldSettings, + SearchSettings newSettings) { + byte[] bytes = getBytes(oldSettings.getSearchFormat(), text, oldSettings); + if (bytes == null) { + return ""; + } + boolean padNegative = shouldPadNegative(text); + String valueString = getValueString(bytes, newSettings, padNegative); + return valueString.replaceAll(",", ""); + } + + private boolean shouldPadNegative(String text) { + if (text.isBlank()) { + return false; + } + int lastIndexOf = text.trim().lastIndexOf(" "); + if (lastIndexOf < 0) { + // only pad negative if there is only one word in the text and it begins with '-' + return text.charAt(0) == '-'; + } + return false; + } + + private byte[] getBytes(SearchFormat oldFormat, String text, SearchSettings settings) { + ByteMatcher byteMatcher = oldFormat.parse(text, settings); + if (byteMatcher instanceof MaskedByteSequenceByteMatcher matcher) { + return matcher.getBytes(); + } + return null; + } + + private byte[] padToByteSize(byte[] bytes, int byteSize, boolean isBigEndian, + boolean padNegative) { + if (bytes.length >= byteSize) { + return bytes; + } + byte[] newBytes = new byte[byteSize]; + if (padNegative) { + Arrays.fill(newBytes, (byte) -1); + } + int startIndex = isBigEndian ? byteSize - bytes.length : 0; + System.arraycopy(bytes, 0, newBytes, startIndex, bytes.length); + + return newBytes; + } + + @Override + public SearchFormatType getFormatType() { + return SearchFormatType.INTEGER; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/FloatSearchFormat.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/FloatSearchFormat.java new file mode 100644 index 0000000000..f212147e7f --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/FloatSearchFormat.java @@ -0,0 +1,200 @@ +/* ### + * 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.features.base.memsearch.format; + +import java.util.StringTokenizer; + +import ghidra.features.base.memsearch.gui.SearchSettings; +import ghidra.features.base.memsearch.matcher.*; +import ghidra.util.HTMLUtilities; + +/** + * {@link SearchFormat} for parsing and display bytes in a float or double format. + */ +class FloatSearchFormat extends SearchFormat { + private String longName; + private int byteSize; + + FloatSearchFormat(String name, String longName, int size) { + super(name); + if (size != 8 && size != 4) { + throw new IllegalArgumentException("Only supports 4 or 8 byte floating point numbers"); + } + this.longName = longName; + this.byteSize = size; + } + + @Override + public ByteMatcher parse(String input, SearchSettings settings) { + input = input.trim(); + if (input.isBlank()) { + return new InvalidByteMatcher(""); + } + + StringTokenizer tokenizer = new StringTokenizer(input); + int tokenCount = tokenizer.countTokens(); + byte[] bytes = new byte[tokenCount * byteSize]; + int bytesPosition = 0; + while (tokenizer.hasMoreTokens()) { + String tok = tokenizer.nextToken(); + NumberParseResult result = parseNumber(tok, settings); + if (result.errorMessage() != null) { + return new InvalidByteMatcher(result.errorMessage(), result.validInput()); + } + System.arraycopy(result.bytes(), 0, bytes, bytesPosition, byteSize); + bytesPosition += byteSize; + } + return new MaskedByteSequenceByteMatcher(input, bytes, settings); + } + + private NumberParseResult parseNumber(String tok, SearchSettings settings) { + if (tok.equals("-") || tok.equals("-.")) { + return new NumberParseResult(null, "Incomplete negative floating point number", true); + } + if (tok.equals(".")) { + return new NumberParseResult(null, "Incomplete floating point number", true); + } + if (tok.endsWith("E") || tok.endsWith("e") || tok.endsWith("E-") || tok.endsWith("e-")) { + return new NumberParseResult(null, "Incomplete floating point number", true); + } + try { + long value = getValue(tok); + return new NumberParseResult(getBytes(value, settings), null, true); + + } + catch (NumberFormatException e) { + return new NumberParseResult(null, "Floating point parse error: " + e.getMessage(), + false); + } + } + + private long getValue(String tok) { + switch (byteSize) { + case 4: + float floatValue = Float.parseFloat(tok); + return Float.floatToIntBits(floatValue); + case 8: + default: + double dvalue = Double.parseDouble(tok); + return Double.doubleToLongBits(dvalue); + } + } + + private byte[] getBytes(long value, SearchSettings settings) { + byte[] bytes = new byte[byteSize]; + for (int i = 0; i < byteSize; i++) { + byte b = (byte) value; + bytes[i] = b; + value >>= 8; + } + if (settings.isBigEndian()) { + reverse(bytes); + } + return bytes; + } + + @Override + public String getToolTip() { + return HTMLUtilities.toHTML( + "Interpret values as a sequence of\n" + longName + " numbers, separated by spaces"); + } + + @Override + public int compareValues(byte[] bytes1, byte[] bytes2, SearchSettings settings) { + boolean isBigEndian = settings.isBigEndian(); + // check each value one at a time, and return the first one different + for (int i = 0; i < bytes1.length / byteSize; i++) { + double value1 = getValue(bytes1, i, isBigEndian); + double value2 = getValue(bytes2, i, isBigEndian); + if (value1 != value2) { + return Double.compare(value1, value2); + } + } + return 0; + } + + public Double getValue(byte[] bytes, int index, boolean isBigEndian) { + long bits = fromBytes(bytes, index, isBigEndian); + switch (byteSize) { + case 4: + float f = Float.intBitsToFloat((int) bits); + return (double) f; + case 8: + default: + return Double.longBitsToDouble(bits); + } + } + + private long fromBytes(byte[] bytes, int index, boolean isBigEndian) { + byte[] bigEndianBytes = new byte[byteSize]; + System.arraycopy(bytes, index * byteSize, bigEndianBytes, 0, byteSize); + if (!isBigEndian) { + reverse(bigEndianBytes); + } + + long value = 0; + for (int i = 0; i < bigEndianBytes.length; i++) { + value = (value << 8) | (bigEndianBytes[i] & 0xff); + } + return value; + } + + @Override + public String getValueString(byte[] bytes, SearchSettings settings) { + StringBuilder buffer = new StringBuilder(); + int numValues = bytes.length / byteSize; + for (int i = 0; i < numValues; i++) { + double value = getValue(bytes, i, settings.isBigEndian()); + buffer.append(Double.toString(value)); + if (i != numValues - 1) { + buffer.append(", "); + } + } + return buffer.toString(); + } + + @Override + public String convertText(String text, SearchSettings oldSettings, SearchSettings newSettings) { + SearchFormat oldFormat = oldSettings.getSearchFormat(); + switch (oldFormat.getFormatType()) { + case BYTE: + return getTextFromBytes(text, oldFormat, oldSettings); + case FLOATING_POINT: + case STRING_TYPE: + case INTEGER: + default: + return isValidText(text, newSettings) ? text : ""; + + } + } + + private String getTextFromBytes(String text, SearchFormat oldFormat, SearchSettings settings) { + ByteMatcher byteMatcher = oldFormat.parse(text, settings); + if ((byteMatcher instanceof MaskedByteSequenceByteMatcher matcher)) { + byte[] bytes = matcher.getBytes(); + if (bytes.length >= byteSize) { + String valueString = getValueString(bytes, settings); + return valueString.replaceAll(",", ""); + } + } + return isValidText(text, settings) ? text : ""; + } + + @Override + public SearchFormatType getFormatType() { + return SearchFormatType.FLOATING_POINT; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/HexSearchFormat.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/HexSearchFormat.java new file mode 100644 index 0000000000..9c56ba244b --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/HexSearchFormat.java @@ -0,0 +1,244 @@ +/* ### + * 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.features.base.memsearch.format; + +import java.util.*; + +import ghidra.features.base.memsearch.gui.SearchSettings; +import ghidra.features.base.memsearch.matcher.*; +import ghidra.util.HTMLUtilities; + +/** + * {@link SearchFormat} for parsing and display bytes in a hex format. This format only + * accepts hex digits or wild card characters. + */ +class HexSearchFormat extends SearchFormat { + + private static final String WILD_CARDS = ".?"; + private static final String VALID_CHARS = "0123456789abcdefABCDEF" + WILD_CARDS; + private static final int MAX_GROUP_SIZE = 16; + + HexSearchFormat() { + super("Hex"); + } + + @Override + public ByteMatcher parse(String input, SearchSettings settings) { + input = input.trim(); + if (input.isBlank()) { + return new InvalidByteMatcher(""); + } + + List byteGroups = getByteGroups(input); + + if (hasInvalidChars(byteGroups)) { + return new InvalidByteMatcher("Invalid character"); + } + + if (checkGroupSize(byteGroups)) { + return new InvalidByteMatcher("Max group size exceeded. Enter to add more."); + } + + List byteList = getByteList(byteGroups, settings); + byte[] bytes = getBytes(byteList); + byte[] masks = getMask(byteList); + return new MaskedByteSequenceByteMatcher(input, bytes, masks, settings); + } + + @Override + public String getToolTip() { + return HTMLUtilities.toHTML("Interpret value as a sequence of\n" + + "hex numbers, separated by spaces.\n" + "Enter '.' or '?' for a wildcard match"); + } + + private byte[] getBytes(List byteList) { + byte[] bytes = new byte[byteList.size()]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = getByte(byteList.get(i)); + } + return bytes; + } + + private byte[] getMask(List byteList) { + byte[] masks = new byte[byteList.size()]; + for (int i = 0; i < masks.length; i++) { + masks[i] = getMask(byteList.get(i)); + } + return masks; + } + + /** + * Returns the search mask for the given hex byte string. Normal hex digits result + * in a "1111" mask and wildcard digits result in a "0000" mask. + */ + private byte getMask(String tok) { + char c1 = tok.charAt(0); + char c2 = tok.charAt(1); + int index1 = WILD_CARDS.indexOf(c1); + int index2 = WILD_CARDS.indexOf(c2); + if (index1 >= 0 && index2 >= 0) { + return (byte) 0x00; + } + if (index1 >= 0 && index2 < 0) { + return (byte) 0x0F; + } + if (index1 < 0 && index2 >= 0) { + return (byte) 0xF0; + } + return (byte) 0xFF; + } + + /** + * Returns the byte value to be used for the given hex bytes. Handles wildcard characters by + * return treating them as 0s. + */ + private byte getByte(String tok) { + char c1 = tok.charAt(0); + char c2 = tok.charAt(1); + // note: the hexValueOf() method will turn wildcard chars into 0s + return (byte) (hexValueOf(c1) * 16 + hexValueOf(c2)); + } + + private List getByteList(List byteGroups, SearchSettings settings) { + List byteList = new ArrayList<>(); + for (String byteGroup : byteGroups) { + List byteStrings = getByteStrings(byteGroup); + if (!settings.isBigEndian()) { + Collections.reverse(byteStrings); + } + byteList.addAll(byteStrings); + } + return byteList; + } + + private List getByteStrings(String token) { + + if (isSingleWildCardChar(token)) { + // normally, a wildcard character represents a nibble. For convenience, if the there + // is a single wild card character surrounded by whitespace, treat it + // as if the entire byte is wild + token += token; + } + else if (token.length() % 2 != 0) { + // pad an odd number of nibbles with 0; assuming users leave off leading 0 + token = "0" + token; + } + + int n = token.length() / 2; + List list = new ArrayList(n); + for (int i = 0; i < n; i++) { + list.add(token.substring(i * 2, i * 2 + 2)); + } + return list; + } + + private boolean isSingleWildCardChar(String token) { + if (token.length() == 1) { + char c = token.charAt(0); + return WILD_CARDS.indexOf(c) >= 0; + } + return false; + } + + private boolean hasInvalidChars(List byteGroups) { + for (String byteGroup : byteGroups) { + if (hasInvalidChars(byteGroup)) { + return true; + } + } + return false; + } + + private boolean checkGroupSize(List byteGroups) { + for (String byteGroup : byteGroups) { + if (byteGroup.length() > MAX_GROUP_SIZE) { + return true; + } + } + return false; + } + + private List getByteGroups(String input) { + List list = new ArrayList(); + StringTokenizer st = new StringTokenizer(input); + while (st.hasMoreTokens()) { + list.add(st.nextToken()); + } + return list; + } + + private boolean hasInvalidChars(String string) { + for (int i = 0; i < string.length(); i++) { + if (VALID_CHARS.indexOf(string.charAt(i)) < 0) { + return true; + } + } + return false; + } + + /** + * Returns the value of the given hex digit character. + */ + private int hexValueOf(char c) { + if ((c >= '0') && (c <= '9')) { + return c - '0'; + } + else if ((c >= 'a') && (c <= 'f')) { + return c - 'a' + 10; + } + else if ((c >= 'A') && (c <= 'F')) { + return c - 'A' + 10; + } + else { + return 0; + } + } + + @Override + public String convertText(String text, SearchSettings oldSettings, SearchSettings newSettings) { + SearchFormat oldFormat = oldSettings.getSearchFormat(); + if (oldFormat.getClass() == getClass()) { + return text; + } + if (oldFormat.getFormatType() != SearchFormatType.STRING_TYPE) { + ByteMatcher byteMatcher = oldFormat.parse(text, oldSettings); + if ((byteMatcher instanceof MaskedByteSequenceByteMatcher matcher)) { + byte[] bytes = matcher.getBytes(); + byte[] mask = matcher.getMask(); + return getMaskedInputString(bytes, mask); + } + } + + return isValidText(text, newSettings) ? text : ""; + } + + private String getMaskedInputString(byte[] bytes, byte[] mask) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + String s = String.format("%02x", bytes[i]); + builder.append((mask[i] & 0xf0) == 0 ? "." : s.charAt(0)); + builder.append((mask[i] & 0x0f) == 0 ? "." : s.charAt(1)); + builder.append(" "); + } + + return builder.toString().trim(); + } + + @Override + public SearchFormatType getFormatType() { + return SearchFormatType.BYTE; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/NumberParseResult.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/NumberParseResult.java new file mode 100644 index 0000000000..632e6e6da3 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/NumberParseResult.java @@ -0,0 +1,24 @@ +/* ### + * 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.features.base.memsearch.format; + +/** + * Used by the NumberSearchFormat and the FloatSearchFormat for intermediate parsing results. + * @param bytes The bytes that match the parsed number sequence + * @param errorMessage an optional parsing error message + * @param validInput boolean if the input was valid + */ +record NumberParseResult(byte[] bytes, String errorMessage, boolean validInput) {} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/RegExSearchFormat.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/RegExSearchFormat.java new file mode 100644 index 0000000000..6b4f9a6045 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/RegExSearchFormat.java @@ -0,0 +1,66 @@ +/* ### + * 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.features.base.memsearch.format; + +import java.util.regex.PatternSyntaxException; + +import ghidra.features.base.memsearch.gui.SearchSettings; +import ghidra.features.base.memsearch.matcher.*; + +/** + * {@link SearchFormat} for parsing input as a regular expression. This format can't generate + * bytes or parse results. + */ +class RegExSearchFormat extends SearchFormat { + RegExSearchFormat() { + super("Reg Ex"); + } + + @Override + public ByteMatcher parse(String input, SearchSettings settings) { + input = input.trim(); + if (input.isBlank()) { + return new InvalidByteMatcher(""); + } + + try { + return new RegExByteMatcher(input, settings); + } + catch (PatternSyntaxException e) { + return new InvalidByteMatcher("RegEx Pattern Error: " + e.getDescription(), true); + } + } + + @Override + public String getToolTip() { + return "Interpret value as a regular expression."; + } + + @Override + public String getValueString(byte[] bytes, SearchSettings settings) { + return new String(bytes); + } + + @Override + public String convertText(String text, SearchSettings oldSettings, SearchSettings newSettings) { + return isValidText(text, newSettings) ? text : ""; + } + + @Override + public SearchFormatType getFormatType() { + return SearchFormatType.STRING_TYPE; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/SearchFormat.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/SearchFormat.java new file mode 100644 index 0000000000..5dc29b7e5f --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/SearchFormat.java @@ -0,0 +1,159 @@ +/* ### + * 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.features.base.memsearch.format; + +import ghidra.features.base.memsearch.gui.SearchSettings; +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +/** + * SearchFormats are responsible for parsing user input data into a {@link ByteMatcher} that + * can be used for searching memory. It also can convert search matches back into string data and + * can convert string data from other formats into string data for this format. + */ + +public abstract class SearchFormat { + //@formatter:off + public static SearchFormat HEX = new HexSearchFormat(); + public static SearchFormat BINARY = new BinarySearchFormat(); + public static SearchFormat DECIMAL = new DecimalSearchFormat(); + + public static SearchFormat STRING = new StringSearchFormat(); + public static SearchFormat REG_EX = new RegExSearchFormat(); + + public static SearchFormat FLOAT = new FloatSearchFormat("Float", "Floating Point", 4); + public static SearchFormat DOUBLE = new FloatSearchFormat("Double", "Floating Point (8)", 8); + //@formatter:on + + public static SearchFormat[] ALL = + { HEX, BINARY, DECIMAL, STRING, REG_EX, FLOAT, DOUBLE }; + + // SearchFormats fall into one of 4 types + public enum SearchFormatType { + BYTE, INTEGER, FLOATING_POINT, STRING_TYPE + } + + private final String name; + + protected SearchFormat(String name) { + this.name = name; + } + + /** + * Parse the given input and settings into a {@link ByteMatcher} + * @param input the user input string + * @param settings the current search/parse settings + * @return a ByteMatcher that can be used for searching bytes (or an error version of a matcher) + */ + public abstract ByteMatcher parse(String input, SearchSettings settings); + + /** + * Returns a tool tip describing this search format + * @return a tool tip describing this search format + */ + public abstract String getToolTip(); + + /** + * Returns the name of the search format. + * @return the name of the search format + */ + public String getName() { + return name; + } + + @Override + public String toString() { + return getName(); + } + + /** + * Reverse parses the bytes back into input value strings. Note that this is only used by + * numerical and string type formats. Byte oriented formats just return an empty string. + * @param bytes the to convert back into input value strings + * @param settings The search settings used to parse the input into bytes + * @return the string of the reversed parsed byte values + */ + public String getValueString(byte[] bytes, SearchSettings settings) { + return ""; + } + + /** + * Returns a new search input string, doing its best to convert an input string that + * was parsed by a previous {@link SearchFormat}. When it makes sense to do so, it will + * re-interpret the parsed bytes from the old format and reconstruct the input from those + * bytes. This allows the user to do conversions, for example, from numbers to hex or binary and + * vise-versa. If the byte conversion doesn't make sense based on the old and new formats, it + * will use the original input if that input can be parsed by the new input. Finally, if all + * else fails, the new input will be the empty string. + * + * @param text the old input that is parsable by the old format + * @param oldSettings the search settings used to parse the old text + * @param newSettings the search settings to used for the new text + * @return the "best" text to change the user search input to + */ + public abstract String convertText(String text, SearchSettings oldSettings, + SearchSettings newSettings); + + /** + * Returns the {@link SearchFormatType} for this format. This is used to help with the + * {@link #convertText(String, SearchSettings, SearchSettings)} method. + * @return the type for this format + */ + public abstract SearchFormatType getFormatType(); + + /** + * Compares bytes from search results based on how this format interprets the bytes. + * By default, formats just compare the bytes one by one as if they were unsigned values. + * SearchFormats whose bytes represent numerical values will override this method and + * compare the bytes after interpreting them as numerical values. + * + * @param bytes1 the first array of bytes to compare + * @param bytes2 the second array of bytes to compare + * @param settings the search settings used to generate the bytes. + * + * @return a negative integer, zero, or a positive integer as the first byte array + * is less than, equal to, or greater than the second byte array + * + */ + public int compareValues(byte[] bytes1, byte[] bytes2, SearchSettings settings) { + return compareBytesUnsigned(bytes1, bytes2); + } + + protected void reverse(byte[] bytes) { + for (int i = 0; i < bytes.length / 2; i++) { + int swapIndex = bytes.length - 1 - i; + byte tmp = bytes[i]; + bytes[i] = bytes[swapIndex]; + bytes[swapIndex] = tmp; + } + } + + private int compareBytesUnsigned(byte[] oldBytes, byte[] newBytes) { + for (int i = 0; i < oldBytes.length; i++) { + int value1 = oldBytes[i] & 0xff; + int value2 = newBytes[i] & 0xff; + if (value1 != value2) { + return value1 - value2; + } + } + return 0; + } + + protected boolean isValidText(String text, SearchSettings settings) { + ByteMatcher byteMatcher = parse(text, settings); + return byteMatcher.isValidSearch(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/StringSearchFormat.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/StringSearchFormat.java new file mode 100644 index 0000000000..2f653af8da --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/format/StringSearchFormat.java @@ -0,0 +1,145 @@ +/* ### + * 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.features.base.memsearch.format; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import ghidra.features.base.memsearch.gui.SearchSettings; +import ghidra.features.base.memsearch.matcher.*; +import ghidra.util.StringUtilities; + +/** + * {@link SearchFormat} for parsing and display bytes in a string format. This format uses + * several values from SearchSettings included character encoding, case sensitive, and escape + * sequences. + */ +class StringSearchFormat extends SearchFormat { + private final byte CASE_INSENSITIVE_MASK = (byte) 0xdf; + + StringSearchFormat() { + super("String"); + } + + @Override + public ByteMatcher parse(String input, SearchSettings settings) { + input = input.trim(); + if (input.isBlank()) { + return new InvalidByteMatcher(""); + } + + boolean isBigEndian = settings.isBigEndian(); + int inputLength = input.length(); + Charset charset = settings.getStringCharset(); + if (charset == StandardCharsets.UTF_16) { + charset = isBigEndian ? StandardCharsets.UTF_16BE : StandardCharsets.UTF_16LE; + } + + // Escape sequences in the "input" are 2 Characters long. + if (settings.useEscapeSequences() && inputLength >= 2) { + input = StringUtilities.convertEscapeSequences(input); + } + byte[] bytes = input.getBytes(charset); + byte[] maskArray = new byte[bytes.length]; + Arrays.fill(maskArray, (byte) 0xff); + + if (!settings.isCaseSensitive()) { + createCaseInsensitiveBytesAndMasks(charset, bytes, maskArray); + } + + return new MaskedByteSequenceByteMatcher(input, bytes, maskArray, settings); + } + + private void createCaseInsensitiveBytesAndMasks(Charset encodingCharSet, byte[] bytes, + byte[] masks) { + int i = 0; + while (i < bytes.length) { + if (encodingCharSet == StandardCharsets.US_ASCII && + Character.isLetter(bytes[i])) { + masks[i] = CASE_INSENSITIVE_MASK; + bytes[i] = (byte) (bytes[i] & CASE_INSENSITIVE_MASK); + i++; + } + else if (encodingCharSet == StandardCharsets.UTF_8) { + int numBytes = bytesPerCharUTF8(bytes[i]); + if (numBytes == 1 && Character.isLetter(bytes[i])) { + masks[i] = CASE_INSENSITIVE_MASK; + bytes[i] = (byte) (bytes[i] & CASE_INSENSITIVE_MASK); + } + i += numBytes; + } + // Assumes UTF-16 will return 2 Bytes for each character. + // 4-byte UTF-16 will never satisfy the below checks because + // none of their bytes can ever be 0. + else if (encodingCharSet == StandardCharsets.UTF_16BE) { + if (bytes[i] == (byte) 0x0 && Character.isLetter(bytes[i + 1])) { // Checks if ascii character. + masks[i + 1] = CASE_INSENSITIVE_MASK; + bytes[i + 1] = (byte) (bytes[i + 1] & CASE_INSENSITIVE_MASK); + } + i += 2; + } + else if (encodingCharSet == StandardCharsets.UTF_16LE) { + if (bytes[i + 1] == (byte) 0x0 && Character.isLetter(bytes[i])) { // Checks if ascii character. + masks[i] = CASE_INSENSITIVE_MASK; + bytes[i] = (byte) (bytes[i] & CASE_INSENSITIVE_MASK); + } + i += 2; + } + else { + i++; + } + } + } + + private int bytesPerCharUTF8(byte zByte) { + // This method is intended for UTF-8 encoding. + // The first byte in a sequence of UTF-8 bytes can tell + // us how many bytes make up a char. + int offset = 1; + // If the char is ascii, this loop will be skipped. + while ((zByte & 0x80) != 0x00) { + zByte <<= 1; + offset++; + } + return offset; + } + + @Override + public String getToolTip() { + return "Interpret value as a sequence of characters."; + } + + @Override + public String getValueString(byte[] bytes, SearchSettings settings) { + boolean isBigEndian = settings.isBigEndian(); + Charset charset = settings.getStringCharset(); + if (charset == StandardCharsets.UTF_16) { + charset = isBigEndian ? StandardCharsets.UTF_16BE : StandardCharsets.UTF_16LE; + } + return new String(bytes, charset); + } + + @Override + public String convertText(String text, SearchSettings oldSettings, SearchSettings newSettings) { + return isValidText(text, newSettings) ? text : ""; + } + + @Override + public SearchFormatType getFormatType() { + return SearchFormatType.STRING_TYPE; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/CombinedMatchTableLoader.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/CombinedMatchTableLoader.java new file mode 100644 index 0000000000..07950fbca2 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/CombinedMatchTableLoader.java @@ -0,0 +1,75 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.util.Collection; +import java.util.List; + +import ghidra.features.base.memsearch.combiner.Combiner; +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.features.base.memsearch.searcher.MemorySearcher; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.datastruct.ListAccumulator; +import ghidra.util.task.TaskMonitor; + +/** + * Table loader that performs a search and then combines the new results with existing results. + */ +public class CombinedMatchTableLoader implements MemoryMatchTableLoader { + private MemorySearcher memSearcher; + private List previousResults; + private Combiner combiner; + private boolean completedSearch; + private MemoryMatch firstMatch; + + public CombinedMatchTableLoader(MemorySearcher memSearcher, + List previousResults, Combiner combiner) { + this.memSearcher = memSearcher; + this.previousResults = previousResults; + this.combiner = combiner; + } + + @Override + public void loadResults(Accumulator accumulator, TaskMonitor monitor) { + ListAccumulator listAccumulator = new ListAccumulator<>(); + completedSearch = memSearcher.findAll(listAccumulator, monitor); + List followOnResults = listAccumulator.asList(); + firstMatch = followOnResults.isEmpty() ? null : followOnResults.get(0); + Collection results = combiner.combine(previousResults, followOnResults); + accumulator.addAll(results); + } + + @Override + public boolean didTerminateEarly() { + return !completedSearch; + } + + @Override + public void dispose() { + previousResults = null; + } + + @Override + public MemoryMatch getFirstMatch() { + return firstMatch; + } + + @Override + public boolean hasResults() { + return firstMatch != null; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/EmptyMemoryMatchTableLoader.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/EmptyMemoryMatchTableLoader.java new file mode 100644 index 0000000000..3c58ee5eab --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/EmptyMemoryMatchTableLoader.java @@ -0,0 +1,51 @@ +/* ### + * 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.features.base.memsearch.gui; + +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.task.TaskMonitor; + +/** + * Table loader for clearing the existing results + */ +public class EmptyMemoryMatchTableLoader implements MemoryMatchTableLoader { + + @Override + public void loadResults(Accumulator accumulator, TaskMonitor monitor) { + return; + } + + @Override + public void dispose() { + // nothing to do + } + + @Override + public boolean didTerminateEarly() { + return false; + } + + @Override + public MemoryMatch getFirstMatch() { + return null; + } + + @Override + public boolean hasResults() { + return false; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/FindOnceTableLoader.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/FindOnceTableLoader.java new file mode 100644 index 0000000000..a6602e7287 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/FindOnceTableLoader.java @@ -0,0 +1,94 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.util.List; + +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.features.base.memsearch.searcher.MemorySearcher; +import ghidra.program.model.address.Address; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.task.TaskMonitor; + +/** + * Table loader for executing an incremental search forwards or backwards and adding that result + * to the table. + */ +public class FindOnceTableLoader implements MemoryMatchTableLoader { + + private MemorySearcher searcher; + private Address address; + private List previousResults; + private MemorySearchResultsPanel panel; + private MemoryMatch match; + private boolean forward; + + public FindOnceTableLoader(MemorySearcher searcher, Address address, + List previousResults, MemorySearchResultsPanel panel, boolean forward) { + this.searcher = searcher; + this.address = address; + this.previousResults = previousResults; + this.panel = panel; + this.forward = forward; + } + + @Override + public void loadResults(Accumulator accumulator, TaskMonitor monitor) { + accumulator.addAll(previousResults); + + match = searcher.findOnce(address, forward, monitor); + + if (match != null) { + MemoryMatch existing = findExisingMatch(match.getAddress()); + if (existing != null) { + existing.updateBytes(match.getBytes()); + } + else { + accumulator.add(match); + } + } + } + + private MemoryMatch findExisingMatch(Address newMatchAddress) { + for (MemoryMatch memoryMatch : previousResults) { + if (newMatchAddress.equals(memoryMatch.getAddress())) { + return memoryMatch; + } + } + return null; + } + + @Override + public boolean didTerminateEarly() { + return false; + } + + @Override + public MemoryMatch getFirstMatch() { + return match; + } + + @Override + public void dispose() { + previousResults = null; + } + + @Override + public boolean hasResults() { + return match != null; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchHighlighter.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchHighlighter.java new file mode 100644 index 0000000000..d36df87fb3 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchHighlighter.java @@ -0,0 +1,242 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.awt.Color; +import java.util.*; + +import org.apache.commons.lang3.ArrayUtils; + +import docking.widgets.fieldpanel.support.Highlight; +import docking.widgets.table.threaded.ThreadedTableModelListener; +import ghidra.app.nav.Navigatable; +import ghidra.app.util.ListingHighlightProvider; +import ghidra.app.util.SearchConstants; +import ghidra.app.util.viewer.field.*; +import ghidra.app.util.viewer.proxy.ProxyObj; +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.CodeUnit; +import ghidra.program.model.listing.Program; + +/** + * Listing highlight provider to highlight memory search results. + */ +public class MemoryMatchHighlighter implements ListingHighlightProvider { + private Navigatable navigatable; + private Program program; + private List sortedResults; + private MemoryMatchTableModel model; + private MemorySearchOptions options; + private MemoryMatch selectedMatch; + + public MemoryMatchHighlighter(Navigatable navigatable, MemoryMatchTableModel model, + MemorySearchOptions options) { + this.model = model; + this.options = options; + this.navigatable = navigatable; + this.program = navigatable.getProgram(); + + model.addThreadedTableModelListener(new ThreadedTableModelListener() { + @Override + public void loadingStarted() { + clearCache(); + } + + @Override + public void loadingFinished(boolean wasCancelled) { + // stub + } + + @Override + public void loadPending() { + clearCache(); + } + + }); + } + + @Override + public Highlight[] createHighlights(String text, ListingField field, int cursorTextOffset) { + if (!options.isShowHighlights()) { + return NO_HIGHLIGHTS; + } + + if (program != navigatable.getProgram()) { + return NO_HIGHLIGHTS; + } + + Class fieldFactoryClass = field.getFieldFactory().getClass(); + if (fieldFactoryClass != BytesFieldFactory.class) { + return NO_HIGHLIGHTS; + } + + ProxyObj proxy = field.getProxy(); + Object obj = proxy.getObject(); + if (!(obj instanceof CodeUnit cu)) { + return NO_HIGHLIGHTS; + } + + Address minAddr = cu.getMinAddress(); + Address maxAddr = cu.getMaxAddress(); + List results = getMatchesInRange(minAddr, maxAddr); + if (results.isEmpty()) { + return NO_HIGHLIGHTS; + } + + return getHighlights(text, minAddr, results); + } + + private Highlight[] getHighlights(String text, Address minAddr, List results) { + + Highlight[] highlights = new Highlight[results.size()]; + int selectedMatchIndex = -1; + + for (int i = 0; i < highlights.length; i++) { + MemoryMatch match = results.get(i); + Color highlightColor = SearchConstants.SEARCH_HIGHLIGHT_COLOR; + if (match == selectedMatch) { + selectedMatchIndex = i; + highlightColor = SearchConstants.SEARCH_HIGHLIGHT_CURRENT_ADDR_COLOR; + } + highlights[i] = createHighlight(match, minAddr, text, highlightColor); + } + + // move the selected match to the end so that it gets painted last and doesn't get + // painted over by the non-active highlights + if (selectedMatchIndex >= 0) { + ArrayUtils.swap(highlights, selectedMatchIndex, highlights.length - 1); + } + + return highlights; + } + + private Highlight createHighlight(MemoryMatch match, Address start, String text, Color color) { + int highlightLength = match.getLength(); + Address address = match.getAddress(); + int startByteOffset = (int) address.subtract(start); + int endByteOffset = startByteOffset + highlightLength - 1; + startByteOffset = Math.max(startByteOffset, 0); + return getHighlight(text, startByteOffset, endByteOffset, color); + } + + private Highlight getHighlight(String text, int start, int end, Color color) { + int charStart = getCharPosition(text, start); + int charEnd = getCharPosition(text, end) + 1; + return new Highlight(charStart, charEnd, color); + + } + + private int getCharPosition(String text, int byteOffset) { + int byteGroupSize = options.getByteGroupSize(); + int byteDelimiterLength = options.getByteDelimiter().length(); + + int groupSize = byteGroupSize * 2 + byteDelimiterLength; + int groupIndex = byteOffset / byteGroupSize; + int groupOffset = byteOffset % byteGroupSize; + + int pos = groupIndex * groupSize + 2 * groupOffset; + return Math.min(text.length() - 1, pos); + } + + List getMatches() { + + if (sortedResults != null) { + return sortedResults; + } + + if (model.isBusy()) { + return Collections.emptyList(); + } + + List modelData = model.getModelData(); + if (model.isSortedOnAddress()) { + return modelData; + } + + sortedResults = new ArrayList<>(modelData); + Collections.sort(sortedResults); + + return sortedResults; + } + + private List getMatchesInRange(Address start, Address end) { + List matches = getMatches(); + int startIndex = findFirstIndex(matches, start, end); + if (startIndex < 0) { + return Collections.emptyList(); + } + + int endIndex = findIndexAtOrGreater(matches, end); + if (endIndex < matches.size() && (matches.get(endIndex).getAddress().equals(end))) { + endIndex++; // end index is non-inclusive and we want to include direct hit + } + + List resultList = matches.subList(startIndex, endIndex); + return resultList; + } + + private int findFirstIndex(List matches, Address start, Address end) { + + int startIndex = findIndexAtOrGreater(matches, start); + if (startIndex > 0) { // see if address before extends into this range. + MemoryMatch resultBefore = matches.get(startIndex - 1); + Address beforeAddr = resultBefore.getAddress(); + int length = resultBefore.getLength(); + if (start.hasSameAddressSpace(beforeAddr) && start.subtract(beforeAddr) < length) { + return startIndex - 1; + } + } + + if (startIndex == matches.size()) { + return -1; + } + + MemoryMatch result = matches.get(startIndex); + Address addr = result.getAddress(); + if (end.compareTo(addr) >= 0) { + return startIndex; + } + return -1; + } + + private int findIndexAtOrGreater(List matches, Address address) { + + MemoryMatch key = new MemoryMatch(address); + int index = Collections.binarySearch(matches, key); + if (index < 0) { + index = -index - 1; + } + return index; + } + + private void clearCache() { + if (sortedResults != null) { + sortedResults.clear(); + sortedResults = null; + } + } + + void dispose() { + navigatable.removeHighlightProvider(this, program); + clearCache(); + } + + void setSelectedMatch(MemoryMatch selectedMatch) { + this.selectedMatch = selectedMatch; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchTableLoader.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchTableLoader.java new file mode 100644 index 0000000000..6756a3cf5e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchTableLoader.java @@ -0,0 +1,60 @@ +/* ### + * 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.features.base.memsearch.gui; + +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.task.TaskMonitor; + +/** + * Interface for loading the memory search results table. Various implementations handle the + * different cases such as a search all, or a search next, or combining results with a previous + * search, etc. + */ +public interface MemoryMatchTableLoader { + + /** + * Called by the table model to initiate searching and loading using the threaded table models + * threading infrastructure. + * @param accumulator the accumulator to store results that will appear in the results table + * @param monitor the task monitor + */ + public void loadResults(Accumulator accumulator, TaskMonitor monitor); + + /** + * Returns true if the search/loading did not fully complete. (Search limit reached, cancelled + * by user, etc.) + * @return true if the search/loading did not fully complete + */ + public boolean didTerminateEarly(); + + /** + * Cleans up resources + */ + public void dispose(); + + /** + * Returns the first match found. Typically used to navigate the associated navigatable. + * @return the first match found + */ + public MemoryMatch getFirstMatch(); + + /** + * Returns true if at least one match was found. + * @return true if at least one match was found + */ + public boolean hasResults(); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchTableModel.java new file mode 100644 index 0000000000..55e9adf56e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchTableModel.java @@ -0,0 +1,287 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.awt.*; + +import docking.widgets.table.*; +import generic.theme.GThemeDefaults.Colors.Tables; +import ghidra.docking.settings.Settings; +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.features.base.memsearch.matcher.ByteMatcher; +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.util.*; +import ghidra.util.HTMLUtilities; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.table.AddressBasedTableModel; +import ghidra.util.table.column.AbstractGColumnRenderer; +import ghidra.util.table.column.GColumnRenderer; +import ghidra.util.table.field.*; +import ghidra.util.task.TaskMonitor; + +/** + * Table model for memory search results. + */ +public class MemoryMatchTableModel extends AddressBasedTableModel { + private Color CHANGED_COLOR = Tables.ERROR_UNSELECTED; + private Color CHANGED_SELECTED_COLOR = Tables.ERROR_SELECTED; + + private MemoryMatchTableLoader loader; + + MemoryMatchTableModel(ServiceProvider serviceProvider, Program program) { + super("Memory Search", serviceProvider, program, null, true); + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + + descriptor.addVisibleColumn( + DiscoverableTableUtils.adaptColumForModel(this, new AddressTableColumn()), 1, true); + descriptor.addVisibleColumn(new MatchBytesColumn()); + descriptor.addVisibleColumn(new MatchValueColumn()); + descriptor.addVisibleColumn( + DiscoverableTableUtils.adaptColumForModel(this, new LabelTableColumn())); + descriptor.addVisibleColumn( + DiscoverableTableUtils.adaptColumForModel(this, new CodeUnitTableColumn())); + + return descriptor; + } + + @Override + protected void doLoad(Accumulator accumulator, TaskMonitor monitor) + throws CancelledException { + if (loader == null) { + return; + } + loader.loadResults(accumulator, monitor); + loader = null; + } + + void setLoader(MemoryMatchTableLoader loader) { + this.loader = loader; + reload(); + } + + public boolean isSortedOnAddress() { + TableSortState sortState = getTableSortState(); + if (sortState.isUnsorted()) { + return false; + } + + ColumnSortState primaryState = sortState.getAllSortStates().get(0); + DynamicTableColumn column = + getColumn(primaryState.getColumnModelIndex()); + String name = column.getColumnName(); + if (AddressTableColumn.NAME.equals(name)) { + return true; + } + return false; + } + + @Override + public ProgramLocation getProgramLocation(int modelRow, int modelColumn) { + Program p = getProgram(); + if (p == null) { + return null; // we've been disposed + } + + DynamicTableColumn column = getColumn(modelColumn); + Class columnClass = column.getClass(); + if (column instanceof MappedTableColumn mappedColumn) { + columnClass = mappedColumn.getMappedColumnClass(); + } + if (columnClass == AddressTableColumn.class || columnClass == MatchBytesColumn.class || + columnClass == MatchValueColumn.class) { + return new BytesFieldLocation(p, getAddress(modelRow)); + } + + return super.getProgramLocation(modelRow, modelColumn); + } + + @Override + public Address getAddress(int row) { + MemoryMatch result = getRowObject(row); + return result.getAddress(); + } + + @Override + public ProgramSelection getProgramSelection(int[] rows) { + AddressSet addressSet = new AddressSet(); + for (int row : rows) { + MemoryMatch result = getRowObject(row); + int addOn = result.getLength() - 1; + Address minAddr = getAddress(row); + Address maxAddr = minAddr; + try { + maxAddr = minAddr.addNoWrap(addOn); + addressSet.addRange(minAddr, maxAddr); + } + catch (AddressOverflowException e) { + // I guess we don't care--not sure why this is undocumented :( + } + } + return new ProgramSelection(addressSet); + } + + public class MatchBytesColumn + extends DynamicTableColumnExtensionPoint { + + private ByteArrayRenderer renderer = new ByteArrayRenderer(); + + @Override + public String getColumnName() { + return "Match Bytes"; + } + + @Override + public String getValue(MemoryMatch match, Settings settings, Program pgm, + ServiceProvider service) throws IllegalArgumentException { + + return getByteString(match.getBytes()); + } + + private String getByteString(byte[] bytes) { + StringBuilder b = new StringBuilder(); + int max = bytes.length - 1; + for (int i = 0;; i++) { + b.append(String.format("%02x", bytes[i])); + if (i == max) { + break; + } + b.append(" "); + } + return b.toString(); + } + + @Override + public int getColumnPreferredWidth() { + return 200; + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + } + + public class MatchValueColumn + extends DynamicTableColumnExtensionPoint { + + private ValueRenderer renderer = new ValueRenderer(); + + @Override + public String getColumnName() { + return "Match Value"; + } + + @Override + public String getValue(MemoryMatch match, Settings settings, Program pgm, + ServiceProvider service) throws IllegalArgumentException { + + ByteMatcher byteMatcher = match.getByteMatcher(); + SearchSettings searchSettings = byteMatcher.getSettings(); + SearchFormat format = searchSettings.getSearchFormat(); + return format.getValueString(match.getBytes(), searchSettings); + } + + @Override + public int getColumnPreferredWidth() { + return 200; + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + } + + private class ByteArrayRenderer extends AbstractGColumnRenderer { + public ByteArrayRenderer() { + setHTMLRenderingEnabled(true); + } + + @Override + protected Font getDefaultFont() { + return fixedWidthFont; + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + super.getTableCellRendererComponent(data); + MemoryMatch match = (MemoryMatch) data.getRowObject(); + String text = data.getValue().toString(); + if (match.isChanged()) { + text = getHtmlColoredString(match, data.isSelected()); + } + setText(text); + return this; + } + + private String getHtmlColoredString(MemoryMatch match, boolean isSelected) { + Color color = isSelected ? Tables.ERROR_SELECTED : Tables.ERROR_UNSELECTED; + + StringBuilder b = new StringBuilder(); + b.append(""); + byte[] bytes = match.getBytes(); + byte[] previousBytes = match.getPreviousBytes(); + int max = bytes.length - 1; + for (int i = 0;; i++) { + String byteString = String.format("%02x", bytes[i]); + if (bytes[i] != previousBytes[i]) { + byteString = HTMLUtilities.colorString(color, byteString); + } + b.append(byteString); + if (i == max) + break; + b.append(" "); + } + + return b.toString(); + } + + @Override + public String getFilterString(String t, Settings settings) { + // This returns the formatted string without the formatted markup + return t; + } + } + + private class ValueRenderer extends AbstractGColumnRenderer { + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + super.getTableCellRendererComponent(data); + setText((String) data.getValue()); + + MemoryMatch match = (MemoryMatch) data.getRowObject(); + if (match.isChanged()) { + setForeground(data.isSelected() ? CHANGED_SELECTED_COLOR : CHANGED_COLOR); + } + return this; + } + + @Override + public String getFilterString(String t, Settings settings) { + return t; + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchToAddressTableRowMapper.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchToAddressTableRowMapper.java new file mode 100644 index 0000000000..8430b703f8 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchToAddressTableRowMapper.java @@ -0,0 +1,36 @@ +/* ### + * 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.features.base.memsearch.gui; + +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.util.table.ProgramLocationTableRowMapper; + +/** + * Maps {@link MemoryMatch} objects (search result) to an address to pick up address based + * table columns. + */ +public class MemoryMatchToAddressTableRowMapper + extends ProgramLocationTableRowMapper { + + @Override + public Address map(MemoryMatch rowObject, Program data, ServiceProvider serviceProvider) { + return rowObject.getAddress(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchToProgramLocationTableRowMapper.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchToProgramLocationTableRowMapper.java new file mode 100644 index 0000000000..4959d8a7cb --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchToProgramLocationTableRowMapper.java @@ -0,0 +1,37 @@ +/* ### + * 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.features.base.memsearch.gui; + +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.util.table.ProgramLocationTableRowMapper; + +/** + * Maps {@link MemoryMatch} objects (search result) to program locations to pick up + * program location based table columns. + */ +public class MemoryMatchToProgramLocationTableRowMapper + extends ProgramLocationTableRowMapper { + + @Override + public ProgramLocation map(MemoryMatch rowObject, Program program, + ServiceProvider serviceProvider) { + return new ProgramLocation(program, rowObject.getAddress()); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchtToFunctionTableRowMapper.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchtToFunctionTableRowMapper.java new file mode 100644 index 0000000000..92b7bf8257 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryMatchtToFunctionTableRowMapper.java @@ -0,0 +1,37 @@ +/* ### + * 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.features.base.memsearch.gui; + +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.listing.*; +import ghidra.util.table.ProgramLocationTableRowMapper; + +/** + * Maps {@link MemoryMatch} objects (search result) to functions to pick up function based + * table columns. + */ +public class MemoryMatchtToFunctionTableRowMapper + extends ProgramLocationTableRowMapper { + + @Override + public Function map(MemoryMatch rowObject, Program program, + ServiceProvider serviceProvider) { + FunctionManager functionManager = program.getFunctionManager(); + return functionManager.getFunctionContaining(rowObject.getAddress()); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryScanControlPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryScanControlPanel.java new file mode 100644 index 0000000000..82e8522eb6 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemoryScanControlPanel.java @@ -0,0 +1,81 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; + +import javax.swing.*; + +import docking.widgets.button.GRadioButton; +import ghidra.app.util.HelpTopics; +import ghidra.features.base.memsearch.scan.Scanner; +import ghidra.util.HelpLocation; +import help.Help; +import help.HelpService; + +/** + * Internal panel of the memory search window that manages the controls for the scan feature. This + * panel can be added or removed via a toolbar action. Not showing by default. + */ +public class MemoryScanControlPanel extends JPanel { + private Scanner selectedScanner = Scanner.NOT_EQUALS; + private boolean hasResults; + private boolean isBusy; + private JButton scanButton; + + MemoryScanControlPanel(MemorySearchProvider provider) { + super(new BorderLayout()); + setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0)); + add(buildButtonPanel(), BorderLayout.CENTER); + scanButton = new JButton("Scan Values"); + scanButton.setToolTipText("Refreshes byte values of current results and eliminates " + + "those that don't meet the selected change criteria"); + HelpService helpService = Help.getHelpService(); + helpService.registerHelp(this, new HelpLocation(HelpTopics.SEARCH, "Scan_Controls")); + add(scanButton, BorderLayout.WEST); + scanButton.addActionListener(e -> provider.scan(selectedScanner)); + } + + private JComponent buildButtonPanel() { + JPanel panel = new JPanel(new FlowLayout()); + ButtonGroup buttonGroup = new ButtonGroup(); + for (Scanner scanner : Scanner.values()) { + GRadioButton button = new GRadioButton(scanner.getName()); + buttonGroup.add(button); + panel.add(button); + button.setSelected(scanner == selectedScanner); + button.addActionListener(e -> selectedScanner = scanner); + button.setToolTipText(scanner.getDescription()); + } + return panel; + } + + public void setSearchStatus(boolean hasResults, boolean isBusy) { + this.hasResults = hasResults; + this.isBusy = isBusy; + updateScanButton(); + } + + private void updateScanButton() { + scanButton.setEnabled(canScan()); + } + + private boolean canScan() { + return hasResults && !isBusy; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchControlPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchControlPanel.java new file mode 100644 index 0000000000..389a6ff228 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchControlPanel.java @@ -0,0 +1,452 @@ +/* ### + * 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.features.base.memsearch.gui; + +import static ghidra.features.base.memsearch.combiner.Combiner.*; + +import java.awt.*; +import java.awt.event.*; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.text.*; + +import docking.DockingUtils; +import docking.menu.ButtonState; +import docking.menu.MultiStateButton; +import docking.widgets.PopupWindow; +import docking.widgets.combobox.GhidraComboBox; +import docking.widgets.label.GDLabel; +import docking.widgets.list.GComboBoxCellRenderer; +import generic.theme.GThemeDefaults.Colors.Messages; +import ghidra.features.base.memsearch.combiner.Combiner; +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.features.base.memsearch.matcher.ByteMatcher; +import ghidra.features.base.memsearch.matcher.InvalidByteMatcher; +import ghidra.util.HTMLUtilities; +import ghidra.util.Swing; +import ghidra.util.layout.PairLayout; +import ghidra.util.layout.VerticalLayout; +import ghidra.util.timer.GTimer; + +/** + * Internal panel of the memory search window that manages the controls for the search feature. This + * panel can be added or removed via a toolbar action. This panel is showing by default. + */ +class MemorySearchControlPanel extends JPanel { + private MultiStateButton searchButton; + private GhidraComboBox searchInputField; + private GDLabel hexSearchSequenceField; + private boolean hasResults; + private ByteMatcher currentMatcher = new InvalidByteMatcher(""); + private SearchHistory searchHistory; + private SearchGuiModel model; + private JCheckBox selectionCheckbox; + private boolean isBusy; + private MemorySearchProvider provider; + private List> initialSearchButtonStates; + private List> combinerSearchButtonStates; + private JComboBox formatComboBox; + private PopupWindow popup; + private String errorMessage; + + MemorySearchControlPanel(MemorySearchProvider provider, SearchGuiModel model, + SearchHistory history) { + super(new BorderLayout()); + this.provider = provider; + this.searchHistory = history; + this.model = model; + model.addChangeCallback(this::guiModelChanged); + initialSearchButtonStates = createButtonStatesForInitialSearch(); + combinerSearchButtonStates = createButtonStatesForAdditionSearches(); + + setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0)); + add(buildLeftSearchInputPanel(), BorderLayout.CENTER); + add(buildRightSearchInputPanel(), BorderLayout.EAST); + } + + private JComponent buildRightSearchInputPanel() { + JPanel panel = new JPanel(new VerticalLayout(5)); + panel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 0)); + searchButton = new MultiStateButton(initialSearchButtonStates); + searchButton + .setStateChangedListener(state -> model.setMatchCombiner(state.getClientData())); + searchButton.addActionListener(e -> search()); + panel.add(searchButton, BorderLayout.WEST); + selectionCheckbox = new JCheckBox("Selection Only"); + selectionCheckbox.setSelected(model.isSearchSelectionOnly()); + selectionCheckbox.setEnabled(model.hasSelection()); + selectionCheckbox + .setToolTipText("If selected, search will be restricted to selected addresses"); + selectionCheckbox.addActionListener( + e -> model.setSearchSelectionOnly(selectionCheckbox.isSelected())); + panel.add(selectionCheckbox); + searchButton.setEnabled(false); + return panel; + } + + private List> createButtonStatesForAdditionSearches() { + List> states = new ArrayList<>(); + states.add(new ButtonState("New Search", "New Search", + "Replaces the current results with the new search results", REPLACE)); + states.add(new ButtonState("Add To Search", "A union B", + "Adds the results of the new search to the existing results", UNION)); + states.add(new ButtonState("Intersect Search", "A intersect B", + "Keep results that in both the existing and new results", INTERSECT)); + states.add(new ButtonState("Xor Search", "A xor B", + "Keep results that are in either existig or results, but not both", XOR)); + states.add(new ButtonState("A-B Search", "A - B", + "Subtracts the new results from the existing results", A_MINUS_B)); + states.add(new ButtonState("B-A Search", "B - A", + "Subtracts the existing results from the new results.", B_MINUS_A)); + return states; + } + + private List> createButtonStatesForInitialSearch() { + List> states = new ArrayList<>(); + states.add(new ButtonState("Search", "", + "Perform a search for the entered values.", null)); + return states; + } + + private void guiModelChanged(SearchSettings oldSettings) { + SearchFormat searchFormat = model.getSearchFormat(); + if (!formatComboBox.getSelectedItem().equals(searchFormat)) { + formatComboBox.setSelectedItem(searchFormat); + } + selectionCheckbox.setSelected(model.isSearchSelectionOnly()); + selectionCheckbox.setEnabled(model.hasSelection()); + searchInputField.setToolTipText(searchFormat.getToolTip()); + + String text = searchInputField.getText(); + String convertedText = searchFormat.convertText(text, oldSettings, model.getSettings()); + searchInputField.setText(convertedText); + ByteMatcher byteMatcher = searchFormat.parse(convertedText, model.getSettings()); + setByteMatcher(byteMatcher); + } + + private JComponent buildLeftSearchInputPanel() { + createSearchInputField(); + hexSearchSequenceField = new GDLabel(); + hexSearchSequenceField.setName("HexSequenceField"); + Border outerBorder = BorderFactory.createLoweredBevelBorder(); + Border innerBorder = BorderFactory.createEmptyBorder(0, 4, 0, 4); + Border border = BorderFactory.createCompoundBorder(outerBorder, innerBorder); + hexSearchSequenceField.setBorder(border); + + JPanel panel = new JPanel(new PairLayout(2, 10)); + panel.add(buildSearchFormatCombo()); + panel.add(searchInputField); + JLabel byteSequenceLabel = new JLabel("Byte Sequence:", SwingConstants.RIGHT); + byteSequenceLabel.setToolTipText( + "This field shows the byte sequence that will be search (if applicable)"); + + panel.add(byteSequenceLabel); + panel.add(hexSearchSequenceField); + return panel; + } + + private void createSearchInputField() { + searchInputField = new GhidraComboBox<>() { + @Override + public void setSelectedItem(Object obj) { + if (obj instanceof String) { + // this can happen when a user types a string and presses enter + // our data model is ByteMatcher, not strings + return; + } + ByteMatcher matcher = (ByteMatcher) obj; + model.setSettings(matcher.getSettings()); + super.setSelectedItem(obj); + } + }; + updateCombo(); + searchInputField.setAutoCompleteEnabled(false); // this interferes with validation + searchInputField.setEditable(true); + searchInputField.setToolTipText(model.getSearchFormat().getToolTip()); + searchInputField.setDocument(new RestrictedInputDocument()); + searchInputField.addActionListener(ev -> search()); + JTextField searchTextField = searchInputField.getTextField(); + + // add escape key listener to dismiss any error popup windows + searchTextField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(java.awt.event.KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + clearInputError(); + e.consume(); + } + } + }); + + // add focus lost listener to dismiss any error popup windows + searchTextField.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + clearInputError(); + } + }); + searchInputField.setRenderer(new SearchHistoryRenderer()); + } + + private boolean canSearch() { + return !isBusy && currentMatcher.isValidSearch(); + } + + private void search() { + if (canSearch()) { + provider.search(); + searchHistory.addSearch(currentMatcher); + updateCombo(); + } + } + + private JComponent buildSearchFormatCombo() { + formatComboBox = new JComboBox<>(SearchFormat.ALL); + formatComboBox.setSelectedItem(model.getSearchFormat()); + formatComboBox.addItemListener(this::formatComboChanged); + formatComboBox.setToolTipText("The selected format will determine how to " + + "interpret text typed into the input field"); + + return formatComboBox; + } + + private void formatComboChanged(ItemEvent e) { + if (e.getStateChange() != ItemEvent.SELECTED) { + return; + } + SearchFormat newFormat = (SearchFormat) e.getItem(); + SearchSettings oldSettings = model.getSettings(); + SearchSettings newSettings = oldSettings.withSearchFormat(newFormat); + String newText = convertInput(oldSettings, newSettings); + model.setSearchFormat(newFormat); + searchInputField.setText(newText); + } + + String convertInput(SearchSettings oldSettings, SearchSettings newSettings) { + String text = searchInputField.getText(); + SearchFormat newFormat = newSettings.getSearchFormat(); + return newFormat.convertText(text, oldSettings, newSettings); + } + + private void setByteMatcher(ByteMatcher byteMatcher) { + clearInputError(); + currentMatcher = byteMatcher; + String text = currentMatcher.getDescription(); + hexSearchSequenceField.setText(text); + hexSearchSequenceField.setToolTipText(currentMatcher.getToolTip()); + updateSearchButton(); + provider.setByteMatcher(byteMatcher); + } + + void setSearchStatus(boolean hasResults, boolean isBusy) { + this.hasResults = hasResults; + this.isBusy = isBusy; + updateSearchButton(); + } + + private void updateSearchButton() { + searchButton.setEnabled(canSearch()); + if (!hasResults) { + searchButton.setButtonStates(initialSearchButtonStates); + return; + } + Combiner combiner = model.getMatchCombiner(); + searchButton.setButtonStates(combinerSearchButtonStates); + searchButton.setSelectedStateByClientData(combiner); + } + + private void adjustLocationForCaretPosition(Point location) { + JTextField textField = searchInputField.getTextField(); + Caret caret = textField.getCaret(); + Point p = caret.getMagicCaretPosition(); + if (p != null) { + location.x += p.x; + } + } + + private void reportInputError(String message) { + this.errorMessage = message; + + // Sometimes when user input is being processed we will get multiple events, with initial + // events putting our model in a bad state, but with follow-up events correcting the state. + // By showing the error message later, we give the follow-up events a change to fix the + // state and clear the error message which prevents the temporary bad state from actually + // displaying an error message to the user. + + Swing.runLater(this::popupErrorMessage); + } + + private void popupErrorMessage() { + if (errorMessage == null) { + return; + } + errorMessage = null; + DockingUtils.setTipWindowEnabled(false); + + Point location = searchInputField.getLocation(); + adjustLocationForCaretPosition(location); + location.y += searchInputField.getHeight() + 5; + + JToolTip tip = new JToolTip(); + tip.setTipText(errorMessage); + + if (popup != null) { + popup.dispose(); + } + popup = new PopupWindow(tip); + popup.showPopup(searchInputField, location, true); + GTimer.scheduleRunnable(1500, this::clearInputError); + Toolkit.getDefaultToolkit().beep(); + } + + private void clearInputError() { + errorMessage = null; + DockingUtils.setTipWindowEnabled(true); + PopupWindow.hideAllWindows(); + if (popup != null) { + popup.dispose(); + } + } + + private void updateCombo() { + ByteMatcher[] historyArray = searchHistory.getHistoryAsArray(); + + searchInputField.setModel(new DefaultComboBoxModel<>(historyArray)); + } + + /** + * Custom Document that validates user input on the fly. + */ + public class RestrictedInputDocument extends DefaultStyledDocument { + + /** + * Called before new user input is inserted into the entry text field. The super + * method is called if the input is accepted. + */ + @Override + public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { + // allow pasting numbers in forms like 0xABC or ABCh + str = removeNumberBasePrefixAndSuffix(str); + + String currentText = getText(0, getLength()); + String beforeOffset = currentText.substring(0, offs); + String afterOffset = currentText.substring(offs, currentText.length()); + String proposedText = beforeOffset + str + afterOffset; + + ByteMatcher byteMatcher = model.parse(proposedText); + if (!byteMatcher.isValidInput()) { + reportInputError(byteMatcher.getDescription()); + return; + } + super.insertString(offs, str, a); + + setByteMatcher(byteMatcher); + } + + /** + * Called before the user deletes some text. If the result is valid, the super + * method is called. + */ + @Override + public void remove(int offs, int len) throws BadLocationException { + clearInputError(); + + String currentText = getText(0, getLength()); + String beforeOffset = currentText.substring(0, offs); + String afterOffset = currentText.substring(len + offs, currentText.length()); + String proposedResult = beforeOffset + afterOffset; + + if (proposedResult.length() == 0) { + super.remove(offs, len); + setByteMatcher(new InvalidByteMatcher("")); + return; + } + + ByteMatcher byteMatcher = model.parse(proposedResult); + if (!byteMatcher.isValidInput()) { + reportInputError(byteMatcher.getDescription()); + return; + } + super.remove(offs, len); + setByteMatcher(byteMatcher); + } + + private String removeNumberBasePrefixAndSuffix(String str) { + SearchFormat format = model.getSearchFormat(); + if (!(format == SearchFormat.HEX || format == SearchFormat.BINARY)) { + return str; + } + + String numMaybe = str.strip(); + String lowercase = numMaybe.toLowerCase(); + if (format == SearchFormat.HEX) { + if (lowercase.startsWith("0x")) { + numMaybe = numMaybe.substring(2); + } + else if (lowercase.startsWith("$")) { + numMaybe = numMaybe.substring(1); + } + else if (lowercase.endsWith("h")) { + numMaybe = numMaybe.substring(0, numMaybe.length() - 1); + } + } + else { + if (lowercase.startsWith("0b")) { + numMaybe = numMaybe.substring(2); + } + } + + // check if the resultant number looks valid for insertion (i.e. not empty) + if (!numMaybe.isEmpty()) { + return numMaybe; + } + return str; + } + } + + void setSearchInput(String initialInput) { + searchInputField.setText(initialInput); + } + + private class SearchHistoryRenderer extends GComboBoxCellRenderer { + { + setHTMLRenderingEnabled(true); + } + + @Override + public Component getListCellRendererComponent(JList list, + ByteMatcher matcher, int index, + boolean isSelected, boolean cellHasFocus) { + + super.getListCellRendererComponent(list, matcher, index, isSelected, cellHasFocus); + + Font font = getFont(); + int formatSize = Math.max(font.getSize() - 3, 6); + SearchFormat format = matcher.getSettings().getSearchFormat(); + String formatHint = HTMLUtilities.setFontSize(format.getName(), formatSize); + if (!isSelected) { + formatHint = HTMLUtilities.colorString(Messages.HINT, formatHint); + } + + setText("" + matcher.getInput() + " " + formatHint + ""); + return this; + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchOptions.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchOptions.java new file mode 100644 index 0000000000..1f8f61194d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchOptions.java @@ -0,0 +1,139 @@ +/* ### + * 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.features.base.memsearch.gui; + +import static ghidra.GhidraOptions.*; +import static ghidra.app.util.SearchConstants.*; + +import ghidra.GhidraOptions; +import ghidra.app.util.SearchConstants; +import ghidra.app.util.viewer.field.BytesFieldFactory; +import ghidra.framework.options.OptionsChangeListener; +import ghidra.framework.options.ToolOptions; +import ghidra.framework.plugintool.PluginTool; +import ghidra.util.bean.opteditor.OptionsVetoException; + +/** + * Class for managing search tool options. + */ +public class MemorySearchOptions { + private static final String PRE_POPULATE_MEM_SEARCH = "Pre-populate Memory Search"; + private static final String AUTO_RESTRICT_SELECTION = "Auto Restrict Search on Selection"; + + private OptionsChangeListener searchOptionsListener; + private OptionsChangeListener browserOptionsListener; + + private boolean prepopulateSearch = true; + private int searchLimit = DEFAULT_SEARCH_LIMIT; + private boolean highlightMatches = true; + private boolean autoRestrictSelection = true; + private int byteGroupSize; + private String byteDelimiter; + + public MemorySearchOptions(PluginTool tool) { + registerSearchOptions(tool); + registerBrowserOptionsListener(tool); + } + + public MemorySearchOptions() { + } + + public int getByteGroupSize() { + return byteGroupSize; + } + + public String getByteDelimiter() { + return byteDelimiter; + } + + public boolean isShowHighlights() { + return highlightMatches; + } + + public int getSearchLimit() { + return searchLimit; + } + + public boolean isAutoRestrictSelection() { + return autoRestrictSelection; + } + + private void registerSearchOptions(PluginTool tool) { + ToolOptions options = tool.getOptions(SearchConstants.SEARCH_OPTION_NAME); + + options.registerOption(PRE_POPULATE_MEM_SEARCH, prepopulateSearch, null, + "Initializes memory search byte sequence from " + + "the current selection provided the selection is less than 10 bytes."); + options.registerOption(AUTO_RESTRICT_SELECTION, autoRestrictSelection, null, + "Automactically restricts searches to the to the current selection," + + " if a selection exists"); + options.registerOption(SearchConstants.SEARCH_HIGHLIGHT_NAME, highlightMatches, null, + "Toggles highlight search results"); + + options.registerThemeColorBinding(SearchConstants.SEARCH_HIGHLIGHT_COLOR_OPTION_NAME, + SearchConstants.SEARCH_HIGHLIGHT_COLOR.getId(), null, + "The search result highlight color"); + options.registerThemeColorBinding( + SearchConstants.SEARCH_HIGHLIGHT_CURRENT_COLOR_OPTION_NAME, + SearchConstants.SEARCH_HIGHLIGHT_CURRENT_ADDR_COLOR.getId(), null, + "The search result highlight color for the currently selected match"); + + loadSearchOptions(options); + + searchOptionsListener = this::searchOptionsChanged; + options.addOptionsChangeListener(searchOptionsListener); + } + + private void registerBrowserOptionsListener(PluginTool tool) { + ToolOptions options = tool.getOptions(GhidraOptions.CATEGORY_BROWSER_FIELDS); + loadBrowserOptions(options); + browserOptionsListener = this::browserOptionsChanged; + options.addOptionsChangeListener(browserOptionsListener); + + } + + private void loadBrowserOptions(ToolOptions options) { + byteGroupSize = options.getInt(BytesFieldFactory.BYTE_GROUP_SIZE_MSG, 1); + byteDelimiter = options.getString(BytesFieldFactory.DELIMITER_MSG, " "); + } + + private void searchOptionsChanged(ToolOptions options, String optionName, Object oldValue, + Object newValue) { + + if (optionName.equals(OPTION_SEARCH_LIMIT)) { + int limit = (int) newValue; + if (limit <= 0) { + throw new OptionsVetoException("Search limit must be greater than 0"); + } + } + + loadSearchOptions(options); + + } + + private void loadSearchOptions(ToolOptions options) { + searchLimit = options.getInt(OPTION_SEARCH_LIMIT, DEFAULT_SEARCH_LIMIT); + highlightMatches = options.getBoolean(SEARCH_HIGHLIGHT_NAME, true); + autoRestrictSelection = options.getBoolean(AUTO_RESTRICT_SELECTION, true); + prepopulateSearch = options.getBoolean(PRE_POPULATE_MEM_SEARCH, true); + } + + private void browserOptionsChanged(ToolOptions options, String optionName, Object oldValue, + Object newValue) { + + loadBrowserOptions(options); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchOptionsPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchOptionsPanel.java new file mode 100644 index 0000000000..64d90ff0a0 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchOptionsPanel.java @@ -0,0 +1,317 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.ItemEvent; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.border.TitledBorder; +import javax.swing.text.*; + +import docking.widgets.checkbox.GCheckBox; +import docking.widgets.combobox.GComboBox; +import ghidra.app.util.HelpTopics; +import ghidra.docking.util.LookAndFeelUtils; +import ghidra.features.base.memsearch.bytesource.SearchRegion; +import ghidra.program.model.lang.Endian; +import ghidra.util.HelpLocation; +import ghidra.util.layout.PairLayout; +import ghidra.util.layout.VerticalLayout; +import help.Help; +import help.HelpService; + +/** + * Internal panel of the memory search window that manages the controls for the search settings. + * This panel can be added or removed via a toolbar action. Not showing by default. + */ +class MemorySearchOptionsPanel extends JPanel { + private SearchGuiModel model; + private GCheckBox caseSensitiveCheckbox; + private GCheckBox escapeSequencesCheckbox; + private GCheckBox decimalUnsignedCheckbox; + private GComboBox decimalByteSizeCombo; + private GComboBox charsetCombo; + private GComboBox endianessCombo; + private boolean isNimbus; + + MemorySearchOptionsPanel(SearchGuiModel model) { + super(new BorderLayout()); + this.model = model; + + // if the look and feel is Nimbus, the spaceing it too big, so we use less spacing + // between elements. + isNimbus = LookAndFeelUtils.isUsingNimbusUI(); + + JPanel scrolledPanel = new JPanel(new VerticalLayout(isNimbus ? 8 : 16)); + scrolledPanel.setBorder(BorderFactory.createEmptyBorder(10, 5, 5, 5)); + + scrolledPanel.add(buildByteOptionsPanel()); + scrolledPanel.add(buildDecimalOptions()); + scrolledPanel.add(buildStringOptions()); + scrolledPanel.add(buildCodeUnitScopePanel()); + scrolledPanel.add(buildMemorySearchRegionsPanel()); + + JScrollPane scroll = new JScrollPane(scrolledPanel); + scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + + add(scroll, BorderLayout.CENTER); + + model.addChangeCallback(this::guiModelChanged); + HelpService helpService = Help.getHelpService(); + helpService.registerHelp(this, new HelpLocation(HelpTopics.SEARCH, "Options")); + + } + + @Override + public Dimension getPreferredSize() { + Dimension size = super.getPreferredSize(); + size.width += 20; // reserve space for the optional vertical scroll bar + return size; + } + + private JComponent buildMemorySearchRegionsPanel() { + JPanel panel = new JPanel(new VerticalLayout(3)); + panel.setBorder(createBorder("Search Region Filter")); + + List choices = model.getMemoryRegionChoices(); + for (SearchRegion region : choices) { + GCheckBox checkbox = new GCheckBox(region.getName()); + checkbox.setToolTipText(region.getDescription()); + checkbox.setSelected(model.isSelectedRegion(region)); + checkbox.addItemListener(e -> model.selectRegion(region, checkbox.isSelected())); + panel.add(checkbox); + } + return panel; + } + + private JComponent buildDecimalOptions() { + JPanel panel = new JPanel(new VerticalLayout(3)); + panel.setBorder(createBorder("Decimal Options")); + + JPanel innerPanel = new JPanel(new PairLayout(5, 5)); + JLabel label = new JLabel("Size:"); + label.setToolTipText("Size of decimal values in bytes"); + innerPanel.add(label); + + Integer[] decimalSizes = new Integer[] { 1, 2, 3, 4, 5, 6, 7, 8, 16 }; + decimalByteSizeCombo = new GComboBox<>(decimalSizes); + decimalByteSizeCombo.setSelectedItem(4); + decimalByteSizeCombo.addItemListener(this::byteSizeComboChanged); + decimalByteSizeCombo.setToolTipText("Size of decimal values in bytes"); + innerPanel.add(decimalByteSizeCombo); + panel.add(innerPanel); + + decimalUnsignedCheckbox = new GCheckBox("Unsigned"); + decimalUnsignedCheckbox.setToolTipText( + "Sets whether decimal values should be interpreted as unsigned values"); + decimalUnsignedCheckbox.addActionListener( + e -> model.setDecimalUnsigned(decimalUnsignedCheckbox.isSelected())); + + panel.add(decimalUnsignedCheckbox); + return panel; + } + + private void byteSizeComboChanged(ItemEvent e) { + if (e.getStateChange() != ItemEvent.SELECTED) { + return; + } + int byteSize = (Integer) e.getItem(); + model.setDecimalByteSize(byteSize); + } + + private JComponent buildCodeUnitScopePanel() { + JPanel panel = new JPanel(new VerticalLayout(5)); + panel.setBorder(createBorder("Code Type Filter")); + GCheckBox instructionsCheckBox = new GCheckBox("Instructions"); + GCheckBox definedDataCheckBox = new GCheckBox("Defined Data"); + GCheckBox undefinedDataCheckBox = new GCheckBox("Undefined Data"); + instructionsCheckBox.setToolTipText( + "If selected, include matches found in instructions"); + definedDataCheckBox.setToolTipText( + "If selected, include matches found in defined data"); + undefinedDataCheckBox.setToolTipText( + "If selected, include matches found in undefined data"); + instructionsCheckBox.setSelected(model.includeInstructions()); + definedDataCheckBox.setSelected(model.includeDefinedData()); + undefinedDataCheckBox.setSelected(model.includeUndefinedData()); + instructionsCheckBox.addActionListener( + e -> model.setIncludeInstructions(instructionsCheckBox.isSelected())); + definedDataCheckBox.addActionListener( + e -> model.setIncludeDefinedData(definedDataCheckBox.isSelected())); + undefinedDataCheckBox.addActionListener( + e -> model.setIncludeUndefinedData(undefinedDataCheckBox.isSelected())); + panel.add(instructionsCheckBox); + panel.add(definedDataCheckBox); + panel.add(undefinedDataCheckBox); + return panel; + } + + private JComponent buildByteOptionsPanel() { + JPanel panel = new JPanel(new PairLayout(3, 2)); + panel.setBorder(createBorder("Byte Options")); + + String[] endianess = new String[] { "Big", "Little" }; + endianessCombo = new GComboBox<>(endianess); + endianessCombo.setSelectedIndex(model.isBigEndian() ? 0 : 1); + endianessCombo.addItemListener(this::endianessComboChanged); + endianessCombo.setToolTipText("Selects the endianess"); + + JTextField alignField = new JTextField(5); + alignField.setDocument(new RestrictedInputDocument()); + alignField.setName("Alignment"); + alignField.setText(Integer.toString(model.getAlignment())); + alignField.setToolTipText( + "Filters out matches whose address is not divisible by the alignment value"); + + panel.add(new JLabel("Endianess:")); + panel.add(endianessCombo); + panel.add(new JLabel("Alignment:")); + panel.add(alignField); + + return panel; + } + + private void endianessComboChanged(ItemEvent e) { + if (e.getStateChange() != ItemEvent.SELECTED) { + return; + } + String endianString = (String) e.getItem(); + Endian endian = Endian.toEndian(endianString); + model.setBigEndian(endian.isBigEndian()); + } + + private JComponent buildStringOptions() { + JPanel panel = new JPanel(new VerticalLayout(3)); + Charset[] supportedCharsets = + { StandardCharsets.US_ASCII, StandardCharsets.UTF_8, StandardCharsets.UTF_16 }; + + charsetCombo = new GComboBox<>(supportedCharsets); + charsetCombo.setName("Encoding Options"); + charsetCombo.setSelectedIndex(0); + charsetCombo.addItemListener(this::encodingComboChanged); + charsetCombo.setToolTipText("Character encoding for translating strings to bytes"); + + JPanel innerPanel = new JPanel(new PairLayout(5, 5)); + JLabel label = new JLabel("Encoding:"); + label.setToolTipText("Character encoding for translating strings to bytes"); + innerPanel.add(label); + innerPanel.add(charsetCombo); + panel.add(innerPanel); + + caseSensitiveCheckbox = new GCheckBox("Case Sensitive"); + caseSensitiveCheckbox.setSelected(model.isCaseSensitive()); + caseSensitiveCheckbox.setToolTipText("Allows for case sensitive searching."); + caseSensitiveCheckbox.addActionListener( + e -> model.setCaseSensitive(caseSensitiveCheckbox.isSelected())); + + escapeSequencesCheckbox = new GCheckBox("Escape Sequences"); + escapeSequencesCheckbox.setSelected(model.useEscapeSequences()); + escapeSequencesCheckbox.setToolTipText( + "Allows specifying control characters using escape sequences " + + "(i.e., allows \\n to be searched for as a single line feed character)."); + escapeSequencesCheckbox.addActionListener( + e -> model.setUseEscapeSequences(escapeSequencesCheckbox.isSelected())); + + panel.setBorder(createBorder("String Options")); + panel.add(caseSensitiveCheckbox); + panel.add(escapeSequencesCheckbox); + return panel; + } + + private void encodingComboChanged(ItemEvent e) { + if (e.getStateChange() != ItemEvent.SELECTED) { + return; + } + Charset charSet = (Charset) e.getItem(); + model.setStringCharset(charSet); + } + + private void guiModelChanged(SearchSettings oldSettings) { + endianessCombo.setSelectedItem(model.isBigEndian() ? "Big" : "Little"); + caseSensitiveCheckbox.setSelected(model.isCaseSensitive()); + escapeSequencesCheckbox.setSelected(model.useEscapeSequences()); + decimalByteSizeCombo.setSelectedItem(model.getDecimalByteSize()); + decimalUnsignedCheckbox.setSelected(model.isDecimalUnsigned()); + charsetCombo.setSelectedItem(model.getStringCharset()); + } + + private Border createBorder(String name) { + TitledBorder outerBorder = BorderFactory.createTitledBorder(name); + if (isNimbus) { + return outerBorder; + } + Border innerBorder = BorderFactory.createEmptyBorder(5, 5, 5, 5); + return BorderFactory.createCompoundBorder(outerBorder, innerBorder); + } + + /** + * Custom Document that validates user input on the fly. + */ + private class RestrictedInputDocument extends DefaultStyledDocument { + + /** + * Called before new user input is inserted into the entry text field. The super + * method is called if the input is accepted. + */ + @Override + public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { + + String currentText = getText(0, getLength()); + String beforeOffset = currentText.substring(0, offs); + String afterOffset = currentText.substring(offs, currentText.length()); + String proposedText = beforeOffset + str + afterOffset; + int alignment = getValue(proposedText); + if (alignment > 0) { + super.insertString(offs, str, a); + model.setAlignment(alignment); + } + + } + + @Override + public void remove(int offs, int len) throws BadLocationException { + + String currentText = getText(0, getLength()); + String beforeOffset = currentText.substring(0, offs); + String afterOffset = currentText.substring(len + offs, currentText.length()); + String proposedResult = beforeOffset + afterOffset; + int alignment = getValue(proposedResult); + if (alignment > 0) { + super.remove(offs, len); + model.setAlignment(alignment); + } + } + + private int getValue(String proposedText) { + if (proposedText.isBlank()) { + return 1; + } + try { + return Integer.parseInt(proposedText); + } + catch (NumberFormatException e) { + return -1; + } + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchPlugin.java new file mode 100644 index 0000000000..078deac018 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchPlugin.java @@ -0,0 +1,252 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +import javax.swing.KeyStroke; + +import docking.action.builder.ActionBuilder; +import ghidra.app.CorePluginPackage; +import ghidra.app.context.NavigatableActionContext; +import ghidra.app.events.ProgramSelectionPluginEvent; +import ghidra.app.nav.Navigatable; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.services.*; +import ghidra.app.util.HelpTopics; +import ghidra.app.util.query.TableService; +import ghidra.features.base.memsearch.bytesource.AddressableByteSource; +import ghidra.features.base.memsearch.matcher.ByteMatcher; +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.features.base.memsearch.searcher.MemorySearcher; +import ghidra.framework.options.SaveState; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.listing.CodeUnit; +import ghidra.program.model.listing.Program; +import ghidra.program.util.*; +import ghidra.util.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.*; + +/** + * Plugin for searching program memory. + */ +//@formatter:off +@PluginInfo( + status = PluginStatus.RELEASED, + packageName = CorePluginPackage.NAME, + category = PluginCategoryNames.SEARCH, + shortDescription = "Search bytes in memory", + description = "This plugin searches bytes in memory. The search " + + "is based on a value entered as hex or decimal numbers, or strings." + + " The value may contain \"wildcards\" or regular expressions" + + " that will match any byte or nibble.", + servicesRequired = { ProgramManager.class, GoToService.class, TableService.class, CodeViewerService.class }, + servicesProvided = { MemorySearchService.class }, + eventsConsumed = { ProgramSelectionPluginEvent.class } +) +//@formatter:on +public class MemorySearchPlugin extends Plugin implements MemorySearchService { + private static final int MAX_HISTORY = 10; + private static final String SHOW_OPTIONS_PANEL = "Show Options Panel"; + private static final String SHOW_SCAN_PANEL = "Show Scan Panel"; + + private ByteMatcher lastByteMatcher; + private MemorySearchOptions options; + private SearchHistory searchHistory = new SearchHistory(MAX_HISTORY); + private Address lastSearchAddress; + + private boolean showScanPanel; + + private boolean showOptionsPanel; + + public MemorySearchPlugin(PluginTool tool) { + super(tool); + createActions(); + options = new MemorySearchOptions(tool); + } + + private void createActions() { + new ActionBuilder("Memory Search", getName()) + .menuPath("&Search", "&Memory...") + .menuGroup("search", "a") + .keyBinding("s") + .description("Search Memory for byte sequence") + .helpLocation(new HelpLocation(HelpTopics.SEARCH, "Memory Search")) + .withContext(NavigatableActionContext.class, true) + .onAction(this::showSearchMemoryProvider) + .buildAndInstall(tool); + + new ActionBuilder("Repeat Memory Search Forwards", getName()) + .menuPath("&Search", "Repeat Search &Forwards") + .menuGroup("search", "b") + .keyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0)) + .description("Repeat last memory search fowards once") + .helpLocation(new HelpLocation(HelpTopics.SEARCH, "Repeat Search Forwards")) + .withContext(NavigatableActionContext.class, true) + .enabledWhen(c -> lastByteMatcher != null && c.getAddress() != null) + .onAction(c -> searchOnce(c, true)) + .buildAndInstall(tool); + + new ActionBuilder("Repeat Memory Search Backwards", getName()) + .menuPath("&Search", "Repeat Search &Backwards") + .menuGroup("search", "c") + .keyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_F3, InputEvent.SHIFT_DOWN_MASK)) + .description("Repeat last memory search backwards once") + .helpLocation(new HelpLocation(HelpTopics.SEARCH, "Repeat Search Backwards")) + .withContext(NavigatableActionContext.class, true) + .enabledWhen(c -> lastByteMatcher != null && c.getAddress() != null) + .onAction(c -> searchOnce(c, false)) + .buildAndInstall(tool); + + } + + private void showSearchMemoryProvider(NavigatableActionContext c) { + SearchSettings settings = lastByteMatcher != null ? lastByteMatcher.getSettings() : null; + SearchHistory copy = new SearchHistory(searchHistory); + MemorySearchProvider provider = + new MemorySearchProvider(this, c.getNavigatable(), settings, options, copy); + + provider.showOptions(showOptionsPanel); + provider.showScanPanel(showScanPanel); + } + + private void searchOnce(NavigatableActionContext c, boolean forward) { + SearchOnceTask task = new SearchOnceTask(c.getNavigatable(), forward); + TaskLauncher.launch(task); + } + + void updateByteMatcher(ByteMatcher matcher) { + lastByteMatcher = matcher; + searchHistory.addSearch(matcher); + } + + @Override + public void readConfigState(SaveState saveState) { + showOptionsPanel = saveState.getBoolean(SHOW_OPTIONS_PANEL, false); + showScanPanel = saveState.getBoolean(SHOW_SCAN_PANEL, false); + } + + @Override + public void writeConfigState(SaveState saveState) { + saveState.putBoolean(SHOW_OPTIONS_PANEL, showOptionsPanel); + saveState.putBoolean(SHOW_SCAN_PANEL, showOptionsPanel); + } +//================================================================================================== +// MemorySearchService methods +//================================================================================================== + + @Override + public void createMemorySearchProvider(Navigatable navigatable, String input, + SearchSettings settings, boolean useSelection) { + + SearchHistory copy = new SearchHistory(searchHistory); + MemorySearchProvider provider = + new MemorySearchProvider(this, navigatable, settings, options, copy); + provider.setSearchInput(input); + provider.setSearchSelectionOnly(false); + + // Custom providers may use input and settings that are fairly unique and not chosen + // by the user directly. We therefore don't want those settings to be reported back for + // adding to the default settings state and history and thereby affecting future normal + // memory searches. + provider.setPrivate(); + } + + private class SearchOnceTask extends Task { + + private Navigatable navigatable; + private boolean forward; + + public SearchOnceTask(Navigatable navigatable, boolean forward) { + super("Search Next", true, true, true); + this.navigatable = navigatable; + this.forward = forward; + } + + private AddressSet getSearchAddresses() { + SearchSettings settings = lastByteMatcher.getSettings(); + AddressSet searchAddresses = settings.getSearchAddresses(navigatable.getProgram()); + ProgramSelection selection = navigatable.getSelection(); + if (selection != null && !selection.isEmpty()) { + searchAddresses = searchAddresses.intersect(navigatable.getSelection()); + } + return searchAddresses; + } + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + AddressableByteSource source = navigatable.getByteSource(); + AddressSet addresses = getSearchAddresses(); + if (addresses.isEmpty()) { + Msg.showWarn(this, null, "Search Failed!", "Addresses to search is empty!"); + return; + } + + Address start = getSearchStartAddress(); + if (start == null) { + Msg.showWarn(this, null, "Search Failed!", "No valid start address!"); + return; + } + MemorySearcher searcher = new MemorySearcher(source, lastByteMatcher, addresses, 1); + + MemoryMatch match = searcher.findOnce(start, forward, monitor); + + Swing.runLater(() -> navigateToMatch(match)); + } + + private Address getSearchStartAddress() { + ProgramLocation location = navigatable.getLocation(); + if (location == null) { + return null; + } + Address start = navigatable.getLocation().getByteAddress(); + if (lastSearchAddress != null) { + CodeUnit cu = navigatable.getProgram().getListing().getCodeUnitContaining(start); + if (cu != null && cu.contains(lastSearchAddress)) { + start = lastSearchAddress; + } + } + return forward ? start.next() : start.previous(); + } + + private void navigateToMatch(MemoryMatch match) { + if (match != null) { + lastSearchAddress = match.getAddress(); + Program program = navigatable.getProgram(); + navigatable.goTo(program, new BytesFieldLocation(program, match.getAddress())); + } + else { + Msg.showWarn(this, null, "Match Not Found", + "No match found going forward for " + lastByteMatcher.getInput()); + } + } + } + + public void setShowOptionsPanel(boolean show) { + showOptionsPanel = show; + + } + + public void setShowScanPanel(boolean show) { + showScanPanel = show; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchProvider.java new file mode 100644 index 0000000000..4cf79e60df --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchProvider.java @@ -0,0 +1,655 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.awt.*; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import javax.swing.*; + +import docking.ActionContext; +import docking.DockingContextListener; +import docking.action.DockingAction; +import docking.action.ToggleDockingAction; +import docking.action.builder.ActionBuilder; +import docking.action.builder.ToggleActionBuilder; +import generic.theme.GIcon; +import ghidra.app.context.NavigatableActionContext; +import ghidra.app.nav.Navigatable; +import ghidra.app.nav.NavigatableRemovalListener; +import ghidra.app.util.HelpTopics; +import ghidra.features.base.memsearch.bytesource.AddressableByteSource; +import ghidra.features.base.memsearch.bytesource.SearchRegion; +import ghidra.features.base.memsearch.matcher.ByteMatcher; +import ghidra.features.base.memsearch.scan.Scanner; +import ghidra.features.base.memsearch.searcher.*; +import ghidra.framework.model.DomainObject; +import ghidra.framework.model.DomainObjectClosedListener; +import ghidra.framework.plugintool.ComponentProviderAdapter; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.listing.CodeUnit; +import ghidra.program.model.listing.Program; +import ghidra.program.util.*; +import ghidra.util.HelpLocation; +import ghidra.util.Msg; +import ghidra.util.layout.VerticalLayout; +import ghidra.util.table.GhidraTable; +import ghidra.util.table.SelectionNavigationAction; +import ghidra.util.table.actions.DeleteTableRowAction; +import ghidra.util.table.actions.MakeProgramSelectionAction; +import resources.Icons; + +/** + * ComponentProvider used to search memory and display search results. + */ +public class MemorySearchProvider extends ComponentProviderAdapter + implements DockingContextListener, NavigatableRemovalListener, DomainObjectClosedListener { + + // @formatter:off + private static final Icon SHOW_SEARCH_PANEL_ICON = new GIcon("icon.base.mem.search.panel.search"); + private static final Icon SHOW_SCAN_PANEL_ICON = new GIcon("icon.base.mem.search.panel.scan"); + private static final Icon SHOW_OPTIONS_ICON = new GIcon("icon.base.mem.search.panel.options"); + // @formatter:on + + private static Set USED_IDS = new HashSet<>(); + + private final int id = getId(); + + private Navigatable navigatable; + private Program program; + private AddressableByteSource byteSource; + + private JComponent mainComponent; + private JPanel controlPanel; + private MemorySearchControlPanel searchPanel; + private MemoryScanControlPanel scanPanel; + private MemorySearchOptionsPanel optionsPanel; + private MemorySearchResultsPanel resultsPanel; + + private ToggleDockingAction toggleOptionsPanelAction; + private ToggleDockingAction toggleScanPanelAction; + private ToggleDockingAction toggleSearchPanelAction; + private DockingAction previousAction; + private DockingAction nextAction; + private DockingAction refreshAction; + + private ByteMatcher byteMatcher; + private Address lastMatchingAddress; + + private boolean isBusy; + private MemoryMatchHighlighter matchHighlighter; + private MemorySearchPlugin plugin; + private MemorySearchOptions options; + private SearchGuiModel model; + private boolean isPrivate = false; + + public MemorySearchProvider(MemorySearchPlugin plugin, Navigatable navigatable, + SearchSettings settings, MemorySearchOptions options, SearchHistory history) { + super(plugin.getTool(), "Memory Search", plugin.getName()); + this.plugin = plugin; + this.navigatable = navigatable; + this.options = options; + this.program = navigatable.getProgram(); + this.byteSource = navigatable.getByteSource(); + + // always initially use the byte ordering of the program, regardless of previous searches + if (settings == null) { + settings = new SearchSettings(); + } + settings = settings.withBigEndian(program.getMemory().isBigEndian()); + + this.model = new SearchGuiModel(settings, byteSource.getSearchableRegions()); + model.setHasSelection(hasSelection(navigatable.getSelection())); + model.setAutoRestrictSelection(options.isAutoRestrictSelection()); + setHelpLocation(new HelpLocation(HelpTopics.SEARCH, "Memory_Search")); + + SearchMarkers markers = new SearchMarkers(tool, getTitle(), program); + searchPanel = new MemorySearchControlPanel(this, model, history); + scanPanel = new MemoryScanControlPanel(this); + optionsPanel = new MemorySearchOptionsPanel(model); + resultsPanel = new MemorySearchResultsPanel(this, markers); + mainComponent = buildMainComponent(); + matchHighlighter = + new MemoryMatchHighlighter(navigatable, resultsPanel.getTableModel(), options); + + setTransient(); + addToTool(); + setVisible(true); + + createActions(plugin.getName()); + + tool.addContextListener(this); + navigatable.addNavigatableListener(this); + program.addCloseListener(this); + updateTitle(); + + } + + public void setSearchInput(String input) { + searchPanel.setSearchInput(input); + } + + public String getSearchInput() { + return byteMatcher == null ? "" : byteMatcher.getInput(); + } + + public void setSearchSelectionOnly(boolean b) { + model.setSearchSelectionOnly(b); + } + + void setPrivate() { + this.isPrivate = true; + } + + private void updateTitle() { + StringBuilder builder = new StringBuilder(); + String searchInput = getSearchInput(); + builder.append("Search Memory: "); + if (!searchInput.isBlank()) { + builder.append("\""); + builder.append(searchInput); + builder.append("\""); + } + builder.append(" ("); + builder.append(getProgramName()); + builder.append(")"); + setTitle(builder.toString()); + } + + @Override + public JComponent getComponent() { + return mainComponent; + } + + void setByteMatcher(ByteMatcher byteMatcher) { + this.byteMatcher = byteMatcher; + tool.contextChanged(this); + } + + /* + * This method will disable "search" actions immediately upon initiating any search or + * scan action. Normally, these actions would enable and disable via context as usual, but + * context changes are issued in a delayed fashion. + */ + void disableActionsFast() { + nextAction.setEnabled(false); + previousAction.setEnabled(false); + refreshAction.setEnabled(false); + } + + boolean canProcessResults() { + return !isBusy && resultsPanel.hasResults(); + } + + private void searchOnce(boolean forward) { + if (hasInvalidSearchSettings()) { + return; + } + updateTitle(); + + Address start = getSearchStartAddress(forward); + AddressSet addresses = getSearchAddresses(); + MemorySearcher searcher = new MemorySearcher(byteSource, byteMatcher, addresses, 1); + searcher.setMatchFilter(createFilter()); + + setBusy(true); + resultsPanel.searchOnce(searcher, start, forward); + + // Only update future memory search settings if this is a standard memory search provider + // because we don't want potentially highly specialized inputs and settings to be in + // the history for standard memory search operations. + if (!isPrivate) { + plugin.updateByteMatcher(byteMatcher); + } + } + + // public so can be called by tests + public void search() { + if (hasInvalidSearchSettings()) { + return; + } + updateTitle(); + int limit = options.getSearchLimit(); + AddressSet addresses = getSearchAddresses(); + MemorySearcher searcher = new MemorySearcher(byteSource, byteMatcher, addresses, limit); + searcher.setMatchFilter(createFilter()); + + setBusy(true); + searchPanel.setSearchStatus(resultsPanel.hasResults(), true); + resultsPanel.search(searcher, model.getMatchCombiner()); + + // Only update future memory search settings if this is a standard memory search provider + // because we don't want potentially highly specialized inputs and settings to be in + // the history for standard memory search operations. + if (!isPrivate) { + plugin.updateByteMatcher(byteMatcher); + } + } + + private boolean hasInvalidSearchSettings() { + Set selectedMemoryRegions = model.getSelectedMemoryRegions(); + if (selectedMemoryRegions.isEmpty()) { + Msg.showInfo(getClass(), resultsPanel, "No Memory Regions Selected!", + "You must select one or more memory regions to perform a search!"); + return true; + } + + if (!(model.includeInstructions() || + model.includeDefinedData() || + model.includeUndefinedData())) { + + Msg.showInfo(getClass(), resultsPanel, "No Code Types Selected!", + "You must select at least one of \"Instructions\"," + + " \"Defined Data\" or \"Undefined Data\" to perform a search!"); + return true; + + } + + return false; + } + + /** + * Performs a scan on the current results, keeping only the results that match the type of scan. + * Note: this method is public to facilitate testing. + * + * @param scanner the scanner to use to reduce the results. + */ + public void scan(Scanner scanner) { + setBusy(true); + resultsPanel.refreshAndMaybeScanForChanges(byteSource, scanner); + } + + private AddressSet getSearchAddresses() { + AddressSet set = model.getSettings().getSearchAddresses(program); + + if (model.isSearchSelectionOnly()) { + set = set.intersect(navigatable.getSelection()); + } + return set; + + } + + private void refreshResults() { + setBusy(true); + resultsPanel.refreshAndMaybeScanForChanges(byteSource, null); + } + + private void setBusy(boolean isBusy) { + this.isBusy = isBusy; + boolean hasResults = resultsPanel.hasResults(); + searchPanel.setSearchStatus(hasResults, isBusy); + scanPanel.setSearchStatus(hasResults, isBusy); + if (isBusy) { + disableActionsFast(); + } + tool.contextChanged(this); + } + + private Predicate createFilter() { + AlignmentFilter alignmentFilter = new AlignmentFilter(model.getAlignment()); + CodeUnitFilter codeUnitFilter = + new CodeUnitFilter(program, model.includeInstructions(), + model.includeDefinedData(), model.includeUndefinedData()); + return alignmentFilter.and(codeUnitFilter); + } + + private Address getSearchStartAddress(boolean forward) { + ProgramLocation location = navigatable.getLocation(); + Address startAddress = location == null ? null : location.getByteAddress(); + if (startAddress == null) { + startAddress = forward ? program.getMinAddress() : program.getMaxAddress(); + } + + /* + Finding the correct starting address is tricky. Ideally, we would just use the + current cursor location's address and begin searching. However, this doesn't work + for subsequent searches for two reasons. + + The first reason is simply that subsequent searches need to start one address past the + current address or else you will just find the same location again. + + The second reason is caused by the way the listing handles arrays. Since arrays don't + have a bytes field, a previous search may have found a hit inside an array, but because + there is no place in the listing to represent that, the cursor is actually placed at + address that is before (possibly several addresses before) the actual hit. So going + forward in the next search, even after incrementing the address, will result in finding + that same hit. + + To solve this, the provider keeps track of a last match address. Subsequent searches + will use this address as long as that address and the cursor address are in the same + code unit. If they are not in the same code unit, we assume the user manually moved the + cursor and want to start searching from that new location. + */ + + if (lastMatchingAddress == null) { + return startAddress; + } + CodeUnit cu = program.getListing().getCodeUnitContaining(startAddress); + if (cu.contains(lastMatchingAddress)) { + startAddress = forward ? lastMatchingAddress.next() : lastMatchingAddress.previous(); + } + if (startAddress == null) { + startAddress = program.getMinAddress(); + } + return startAddress; + } + + void searchAllCompleted(boolean foundResults, boolean cancelled, boolean terminatedEarly) { + setBusy(false); + updateSubTitle(); + if (!cancelled && terminatedEarly) { + Msg.showInfo(getClass(), resultsPanel, "Search Limit Exceeded!", + "Stopped search after finding " + options.getSearchLimit() + " matches.\n" + + "The search limit can be changed at Edit->Tool Options, under Search."); + + } + else if (!foundResults) { + showAlert("No matches found!"); + } + } + + void searchOnceCompleted(MemoryMatch match, boolean cancelled) { + setBusy(false); + updateSubTitle(); + if (match != null) { + lastMatchingAddress = match.getAddress(); + navigatable.goTo(program, new BytesFieldLocation(program, match.getAddress())); + } + else { + showAlert("No Match Found!"); + } + } + + void refreshAndScanCompleted(MemoryMatch match) { + setBusy(false); + updateSubTitle(); + if (match != null) { + lastMatchingAddress = match.getAddress(); + navigatable.goTo(program, new BytesFieldLocation(program, match.getAddress())); + } + } + + @Override + public void componentActivated() { + resultsPanel.providerActivated(); + navigatable.setHighlightProvider(matchHighlighter, program); + } + + private void updateSubTitle() { + StringBuilder builder = new StringBuilder(); + builder.append(" "); + int matchCount = resultsPanel.getMatchCount(); + if (matchCount > 0) { + builder.append("("); + builder.append(matchCount); + builder.append(matchCount == 1 ? " entry)" : " entries)"); + } + setSubTitle(builder.toString()); + } + + private String getProgramName() { + return program.getDomainFile().getName(); + } + + private void updateControlPanel() { + controlPanel.removeAll(); + boolean showSearchPanel = toggleSearchPanelAction.isSelected(); + boolean showScanPanel = toggleScanPanelAction.isSelected(); + + if (showSearchPanel) { + controlPanel.add(searchPanel); + } + if (showSearchPanel && showScanPanel) { + controlPanel.add(new JSeparator()); + } + if (showScanPanel) { + controlPanel.add(scanPanel); + } + controlPanel.revalidate(); + } + + private void toggleShowScanPanel() { + plugin.setShowScanPanel(toggleScanPanelAction.isSelected()); + updateControlPanel(); + } + + private void toggleShowSearchPanel() { + updateControlPanel(); + } + + private void toggleShowOptions() { + plugin.setShowOptionsPanel(toggleOptionsPanelAction.isSelected()); + if (toggleOptionsPanelAction.isSelected()) { + mainComponent.add(optionsPanel, BorderLayout.EAST); + } + else { + mainComponent.remove(optionsPanel); + } + mainComponent.validate(); + } + + private boolean canSearch() { + return !isBusy && byteMatcher != null && byteMatcher.isValidSearch(); + } + + private JComponent buildMainComponent() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setPreferredSize(new Dimension(900, 650)); + panel.add(buildCenterPanel(), BorderLayout.CENTER); + return panel; + } + + private JComponent buildCenterPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.add(buildControlPanel(), BorderLayout.NORTH); + panel.add(resultsPanel, BorderLayout.CENTER); + return panel; + } + + private JComponent buildControlPanel() { + controlPanel = new JPanel(new VerticalLayout(0)); + controlPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10)); + controlPanel.add(searchPanel); + return controlPanel; + } + + private void createActions(String owner) { + + nextAction = new ActionBuilder("Search Next", owner) + .toolBarIcon(Icons.DOWN_ICON) + .toolBarGroup("A") + .description("Search forward for 1 result") + .helpLocation(new HelpLocation(HelpTopics.SEARCH, "Search_Next")) + .enabledWhen(c -> canSearch()) + .onAction(c -> searchOnce(true)) + .buildAndInstallLocal(this); + previousAction = new ActionBuilder("Search Previous", owner) + .toolBarIcon(Icons.UP_ICON) + .toolBarGroup("A") + .description("Search backward for 1 result") + .helpLocation(new HelpLocation(HelpTopics.SEARCH, "Search_Previous")) + .enabledWhen(c -> canSearch()) + .onAction(c -> searchOnce(false)) + .buildAndInstallLocal(this); + + refreshAction = new ActionBuilder("Refresh Results", owner) + .toolBarIcon(Icons.REFRESH_ICON) + .toolBarGroup("A") + .description( + "Reload bytes from memory for each search result and show changes in red") + .helpLocation(new HelpLocation(HelpTopics.SEARCH, "Refresh_Values")) + .enabledWhen(c -> canProcessResults()) + .onAction(c -> refreshResults()) + .buildAndInstallLocal(this); + + toggleSearchPanelAction = new ToggleActionBuilder("Show Memory Search Controls", owner) + .toolBarIcon(SHOW_SEARCH_PANEL_ICON) + .toolBarGroup("Z") + .description("Toggles showing the search controls") + .helpLocation(new HelpLocation(HelpTopics.SEARCH, "Toggle_Search")) + .selected(true) + .onAction(c -> toggleShowSearchPanel()) + .buildAndInstallLocal(this); + + toggleScanPanelAction = new ToggleActionBuilder("Show Memory Scan Controls", owner) + .toolBarIcon(SHOW_SCAN_PANEL_ICON) + .toolBarGroup("Z") + .description("Toggles showing the scan controls") + .helpLocation(new HelpLocation(HelpTopics.SEARCH, "Toggle_Scan")) + .onAction(c -> toggleShowScanPanel()) + .buildAndInstallLocal(this); + + toggleOptionsPanelAction = new ToggleActionBuilder("Show Options", owner) + .toolBarIcon(SHOW_OPTIONS_ICON) + .toolBarGroup("Z") + .description("Toggles showing the search options panel") + .helpLocation(new HelpLocation(HelpTopics.SEARCH, "Toggle_Options")) + .onAction(c -> toggleShowOptions()) + .buildAndInstallLocal(this); + + // add standard table actions + GhidraTable table = resultsPanel.getTable(); + addLocalAction(new MakeProgramSelectionAction(navigatable, owner, table)); + addLocalAction(new SelectionNavigationAction(owner, table)); + addLocalAction(new DeleteTableRowAction(table, owner) { + @Override + public void actionPerformed(ActionContext context) { + super.actionPerformed(context); + updateSubTitle(); + } + }); + + } + + @Override + public void removeFromTool() { + dispose(); + super.removeFromTool(); + } + + private void dispose() { + matchHighlighter.dispose(); + USED_IDS.remove(id); + if (navigatable != null) { + navigatable.removeNavigatableListener(this); + } + resultsPanel.dispose(); + tool.removeContextListener(this); + program.removeCloseListener(this); + } + + @Override + public void contextChanged(ActionContext context) { + model.setHasSelection(hasSelection(navigatable.getSelection())); + } + + private boolean hasSelection(ProgramSelection selection) { + if (selection == null) { + return false; + } + return !selection.isEmpty(); + } + + @Override + public void navigatableRemoved(Navigatable nav) { + closeComponent(); + } + + @Override + public void domainObjectClosed(DomainObject dobj) { + closeComponent(); + } + + Navigatable getNavigatable() { + return navigatable; + } + + private static int getId() { + for (int i = 0; i < Integer.MAX_VALUE; i++) { + if (!USED_IDS.contains(i)) { + USED_IDS.add(i); + return i; + } + } + return 0; + } + + void tableSelectionChanged() { + MemoryMatch selectedMatch = resultsPanel.getSelectedMatch(); + matchHighlighter.setSelectedMatch(selectedMatch); + if (selectedMatch != null) { + lastMatchingAddress = selectedMatch.getAddress(); + } + tool.contextChanged(this); + } + + public void showOptions(boolean b) { + toggleOptionsPanelAction.setSelected(b); + toggleShowOptions(); + } + + public void showScanPanel(boolean b) { + toggleScanPanelAction.setSelected(b); + updateControlPanel(); + } + + public void showSearchPanel(boolean b) { + toggleSearchPanelAction.setSelected(b); + updateControlPanel(); + } + + // testing + public boolean isBusy() { + return isBusy; + } + + public List getSearchResults() { + return resultsPanel.getTableModel().getModelData(); + } + + public void setSettings(SearchSettings settings) { + String converted = searchPanel.convertInput(model.getSettings(), settings); + model.setSettings(settings); + searchPanel.setSearchInput(converted); + } + + public boolean isSearchSelection() { + return model.isSearchSelectionOnly(); + } + + public String getByteString() { + return byteMatcher.getDescription(); + } + + void showAlert(String alertMessage) { + // replace with water mark concept + Toolkit.getDefaultToolkit().beep(); + Msg.showInfo(this, null, "Search Results", alertMessage); + } + + @Override + protected ActionContext createContext(Component sourceComponent, Object contextObject) { + ActionContext context = new NavigatableActionContext(this, navigatable); + context.setContextObject(contextObject); + context.setSourceComponent(sourceComponent); + return context; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchResultsPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchResultsPanel.java new file mode 100644 index 0000000000..7e85963a35 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchResultsPanel.java @@ -0,0 +1,294 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.awt.BorderLayout; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.TableModelEvent; + +import ghidra.app.nav.Navigatable; +import ghidra.features.base.memsearch.bytesource.AddressableByteSource; +import ghidra.features.base.memsearch.combiner.Combiner; +import ghidra.features.base.memsearch.scan.Scanner; +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.features.base.memsearch.searcher.MemorySearcher; +import ghidra.program.model.address.Address; +import ghidra.util.Msg; +import ghidra.util.Swing; +import ghidra.util.exception.CancelledException; +import ghidra.util.table.*; +import ghidra.util.task.*; + +/** + * Internal panel of the memory search window that manages the display of the search results + * in a table. This panel also includes most of the search logic as it has direct access to the + * table for showing the results. + */ +class MemorySearchResultsPanel extends JPanel { + private GhidraThreadedTablePanel threadedTablePanel; + private GhidraTableFilterPanel tableFilterPanel; + private GhidraTable table; + private MemoryMatchTableModel tableModel; + private MemorySearchProvider provider; + private SearchMarkers markers; + + MemorySearchResultsPanel(MemorySearchProvider provider, SearchMarkers markers) { + super(new BorderLayout()); + this.provider = provider; + this.markers = markers; + + Navigatable navigatable = provider.getNavigatable(); + tableModel = new MemoryMatchTableModel(provider.getTool(), navigatable.getProgram()); + threadedTablePanel = new GhidraThreadedTablePanel<>(tableModel); + table = threadedTablePanel.getTable(); + + table.setActionsEnabled(true); + table.installNavigation(provider.getTool(), navigatable); + tableModel.addTableModelListener(this::tableChanged); + + add(threadedTablePanel, BorderLayout.CENTER); + add(createFilterFieldPanel(), BorderLayout.SOUTH); + ListSelectionModel selectionModel = threadedTablePanel.getTable().getSelectionModel(); + selectionModel.addListSelectionListener(this::selectionChanged); + } + + private void tableChanged(TableModelEvent event) { + markers.loadMarkers(provider.getTitle(), tableModel.getModelData()); + } + + void providerActivated() { + markers.makeActiveMarkerSet(); + } + + private void selectionChanged(ListSelectionEvent e) { + if (e.getValueIsAdjusting()) { + return; + } + provider.tableSelectionChanged(); + } + + private JComponent createFilterFieldPanel() { + tableFilterPanel = new GhidraTableFilterPanel<>(table, tableModel); + tableFilterPanel.setToolTipText("Filter search results"); + return tableFilterPanel; + } + + public void search(MemorySearcher searcher, Combiner combiner) { + MemoryMatchTableLoader loader = createLoader(searcher, combiner); + tableModel.addInitialLoadListener( + cancelled -> provider.searchAllCompleted(loader.hasResults(), cancelled, + loader.didTerminateEarly())); + tableModel.setLoader(loader); + } + + public void searchOnce(MemorySearcher searcher, Address address, boolean forward) { + SearchOnceTask task = new SearchOnceTask(forward, searcher, address); + TaskLauncher.launch(task); + } + + public void refreshAndMaybeScanForChanges(AddressableByteSource byteSource, Scanner scanner) { + RefreshAndScanTask task = new RefreshAndScanTask(byteSource, scanner); + TaskLauncher.launch(task); + } + + private MemoryMatchTableLoader createLoader(MemorySearcher searcher, Combiner combiner) { + if (hasResults()) { + + // If we have existing results, the combiner determines how the new search results get + // combined with the existing results. + // + // However, if the combiner is the "Replace" combiner, the results are not combined + // and only the new results are kept. In this case, it is preferred to use the same + // loader as if doing an initial search because you get incremental loading and also + // don't need to copy the existing results to feed to a combiner. + if (combiner != Combiner.REPLACE) { + List previousResults = tableModel.getModelData(); + return new CombinedMatchTableLoader(searcher, previousResults, combiner); + } + } + return new NewSearchTableLoader(searcher); + } + + public boolean hasResults() { + return tableModel.getRowCount() > 0; + } + + public void clearResults() { + tableModel.addInitialLoadListener(b -> provider.searchAllCompleted(true, false, false)); + tableModel.setLoader(new EmptyMemoryMatchTableLoader()); + } + + public int getMatchCount() { + return tableModel.getRowCount(); + } + + void select(MemoryMatch match) { + int rowIndex = tableModel.getRowIndex(match); + if (rowIndex >= 0) { + threadedTablePanel.getTable().selectRow(rowIndex); + } + } + + GhidraTable getTable() { + return table; + } + + public MemoryMatch getSelectedMatch() { + int row = table.getSelectedRow(); + return row < 0 ? null : tableModel.getRowObject(row); + } + + public void dispose() { + markers.dispose(); + tableFilterPanel.dispose(); + } + + MemoryMatchTableModel getTableModel() { + return tableModel; + } + + private class SearchOnceTask extends Task { + + private boolean forward; + private MemorySearcher searcher; + private Address start; + + public SearchOnceTask(boolean forward, MemorySearcher searcher, Address start) { + super(forward ? "Search Next" : "Search Previous", true, true, true); + this.forward = forward; + this.searcher = searcher; + this.start = start; + } + + private void tableLoadComplete(MemoryMatch match, boolean wasCancelled) { + int rowIndex = tableModel.getRowIndex(match); + if (rowIndex >= 0) { + table.selectRow(rowIndex); + table.scrollToSelectedRow(); + provider.searchOnceCompleted(match, wasCancelled); + } + } + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + try { + MemoryMatch match = searcher.findOnce(start, forward, monitor); + if (match != null) { + tableModel.addInitialLoadListener(b -> tableLoadComplete(match, b)); + tableModel.addObject(match); + return; + } + } + catch (Throwable t) { + // Catch any runtime errors so that we exit task gracefully and don't leave + // the provider in a stuck "busy" state. + Msg.showError(this, null, "Unexpected error refreshing bytes", t); + } + Swing.runLater(() -> provider.searchOnceCompleted(null, monitor.isCancelled())); + } + } + + private class RefreshAndScanTask extends Task { + + private AddressableByteSource byteSource; + private Scanner scanner; + + public RefreshAndScanTask(AddressableByteSource byteSource, Scanner scanner) { + super("Refreshing", true, true, true); + this.byteSource = byteSource; + this.scanner = scanner; + } + + private void tableLoadComplete(MemoryMatch match) { + if (match == null) { + provider.refreshAndScanCompleted(null); + } + int rowIndex = tableModel.getRowIndex(match); + if (rowIndex >= 0) { + table.selectRow(rowIndex); + table.scrollToSelectedRow(); + } + provider.refreshAndScanCompleted(match); + } + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + List matches = tableModel.getModelData(); + + if (refreshByteValues(monitor, matches) && scanner != null) { + performScanFiltering(monitor, matches); + } + else { + tableModel.fireTableDataChanged(); // some data bytes may have changed, repaint + provider.refreshAndScanCompleted(null); + } + + } + + private boolean refreshByteValues(TaskMonitor monitor, List matches) { + try { + byteSource.invalidate(); // clear any caches before refreshing byte values + monitor.initialize(matches.size(), "Refreshing..."); + for (MemoryMatch match : matches) { + byte[] bytes = new byte[match.getLength()]; + byteSource.getBytes(match.getAddress(), bytes, bytes.length); + match.updateBytes(bytes); + monitor.incrementProgress(); + if (monitor.isCancelled()) { + return false; + } + } + return true; + } + catch (Throwable t) { + // Catch any runtime errors so that we exit task gracefully and don't leave + // the provider in a stuck "busy" state. + Msg.showError(this, null, "Unexpected error refreshing bytes", t); + } + return false; + } + + private void performScanFiltering(TaskMonitor monitor, List matches) { + monitor.initialize(matches.size(), "Scanning for changes..."); + List scanResults = new ArrayList<>(); + for (MemoryMatch match : matches) { + if (scanner.accept(match)) { + scanResults.add(match); + } + if (monitor.isCancelled()) { + break; + } + } + + MemoryMatch firstIfReduced = getFirstMatchIfReduced(matches, scanResults); + tableModel.addInitialLoadListener(b -> tableLoadComplete(firstIfReduced)); + tableModel.setLoader(new RefreshResultsTableLoader(scanResults)); + } + + private MemoryMatch getFirstMatchIfReduced(List matches, + List scanResults) { + MemoryMatch firstIfReduced = null; + if (!scanResults.isEmpty() && scanResults.size() != matches.size()) { + firstIfReduced = scanResults.isEmpty() ? null : scanResults.getFirst(); + } + return firstIfReduced; + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/NewSearchTableLoader.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/NewSearchTableLoader.java new file mode 100644 index 0000000000..164eda3b2c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/NewSearchTableLoader.java @@ -0,0 +1,67 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.util.Iterator; + +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.features.base.memsearch.searcher.MemorySearcher; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.task.TaskMonitor; + +/** + * Table loader that performs a search and displays the results in the table. + */ +public class NewSearchTableLoader implements MemoryMatchTableLoader { + + private MemorySearcher memSearcher; + private boolean completedSearch; + private MemoryMatch firstMatch; + + NewSearchTableLoader(MemorySearcher memSearcher) { + this.memSearcher = memSearcher; + } + + @Override + public void loadResults(Accumulator accumulator, TaskMonitor monitor) { + completedSearch = memSearcher.findAll(accumulator, monitor); + Iterator iterator = accumulator.iterator(); + if (iterator.hasNext()) { + firstMatch = iterator.next(); + } + } + + @Override + public boolean didTerminateEarly() { + return !completedSearch; + } + + @Override + public void dispose() { + // nothing to do + } + + @Override + public MemoryMatch getFirstMatch() { + return firstMatch; + } + + @Override + public boolean hasResults() { + return firstMatch != null; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/RefreshResultsTableLoader.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/RefreshResultsTableLoader.java new file mode 100644 index 0000000000..fcc5ffce7b --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/RefreshResultsTableLoader.java @@ -0,0 +1,63 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.util.List; + +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.task.TaskMonitor; + +/** + * Table loader that reloads the table with existing results after refreshing the byte values in + * those results. + */ +public class RefreshResultsTableLoader implements MemoryMatchTableLoader { + + private List matches; + private boolean hasResults; + + public RefreshResultsTableLoader(List matches) { + this.matches = matches; + } + + @Override + public void loadResults(Accumulator accumulator, TaskMonitor monitor) { + accumulator.addAll(matches); + hasResults = !accumulator.isEmpty(); + } + + @Override + public void dispose() { + matches.clear(); + matches = null; + } + + @Override + public boolean didTerminateEarly() { + return false; + } + + @Override + public MemoryMatch getFirstMatch() { + return null; + } + + @Override + public boolean hasResults() { + return hasResults; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/SearchGuiModel.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/SearchGuiModel.java new file mode 100644 index 0000000000..a89be401ad --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/SearchGuiModel.java @@ -0,0 +1,287 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.nio.charset.Charset; +import java.util.*; +import java.util.function.Consumer; + +import ghidra.features.base.memsearch.bytesource.SearchRegion; +import ghidra.features.base.memsearch.combiner.Combiner; +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +/** + * Maintains the state of all the settings and controls for the memory search window. + */ +public class SearchGuiModel { + + private SearchSettings settings; + private List> changeCallbacks = new ArrayList<>(); + private boolean hasSelection; + private List regionChoices; + private boolean autoResrictSelection = false; + private boolean isSearchSelectionOnly; + private Combiner combiner; + + public SearchGuiModel(SearchSettings settings, List regionChoices) { + this.regionChoices = regionChoices; + // make a copy of settings so they don't change out from under us + this.settings = settings != null ? settings : new SearchSettings(); + if (!isValidRegionSettings() || this.settings.getSelectedMemoryRegions().isEmpty()) { + installDefaultRegions(); + } + } + + private void installDefaultRegions() { + Set defaultRegions = new HashSet<>(); + for (SearchRegion region : regionChoices) { + if (region.isDefault()) { + defaultRegions.add(region); + } + } + settings = settings.withSelectedRegions(defaultRegions); + } + + private boolean isValidRegionSettings() { + for (SearchRegion region : settings.getSelectedMemoryRegions()) { + if (!regionChoices.contains(region)) { + return false; + } + } + return true; + } + + public void setAutoRestrictSelection() { + autoResrictSelection = true; + } + + public void addChangeCallback(Consumer changeCallback) { + changeCallbacks.add(changeCallback); + } + + private void notifySettingsChanged(SearchSettings oldSettings) { + for (Consumer callback : changeCallbacks) { + callback.accept(oldSettings); + } + } + + public boolean isSearchSelectionOnly() { + return isSearchSelectionOnly; + } + + public boolean hasSelection() { + return hasSelection; + } + + public void setHasSelection(boolean b) { + if (hasSelection == b) { + return; + } + hasSelection = b; + if (b) { + if (autoResrictSelection) { + // autoRestrictSelection means to auto turn on restrict search to selection when + // a selection happens + isSearchSelectionOnly = true; + } + } + else { + // if no selection, then we can't search in a selection! + isSearchSelectionOnly = false; + } + + notifySettingsChanged(settings); + } + + public void setSearchSelectionOnly(boolean b) { + if (isSearchSelectionOnly == b) { + return; + } + // can only set it if there is a current selection + isSearchSelectionOnly = b && hasSelection; + notifySettingsChanged(settings); + } + + public SearchFormat getSearchFormat() { + return settings.getSearchFormat(); + } + + public SearchSettings getSettings() { + return settings; + } + + public void setSearchFormat(SearchFormat searchFormat) { + if (searchFormat == settings.getSearchFormat()) { + return; + } + SearchSettings oldSettings = settings; + settings = settings.withSearchFormat(searchFormat); + notifySettingsChanged(oldSettings); + } + + public ByteMatcher parse(String proposedText) { + return settings.getSearchFormat().parse(proposedText, settings); + } + + public int getAlignment() { + return settings.getAlignment(); + } + + public void setAlignment(int alignment) { + settings = settings.withAlignment(alignment); + // this setting doesn't affect any other gui state, so no need to notify change + } + + public Set getSelectedMemoryRegions() { + return settings.getSelectedMemoryRegions(); + } + + public boolean includeInstructions() { + return settings.includeInstructions(); + } + + public boolean includeDefinedData() { + return settings.includeDefinedData(); + } + + public boolean includeUndefinedData() { + return settings.includeUndefinedData(); + } + + public void setIncludeInstructions(boolean selected) { + settings = settings.withIncludeInstructions(selected); + } + + public void setIncludeDefinedData(boolean selected) { + settings = settings.withIncludeDefinedData(selected); + } + + public void setIncludeUndefinedData(boolean selected) { + settings = settings.withIncludeUndefinedData(selected); + } + + public boolean isBigEndian() { + return settings.isBigEndian(); + } + + public void setBigEndian(boolean b) { + if (settings.isBigEndian() == b) { + return; + } + SearchSettings oldSettings = settings; + settings = settings.withBigEndian(b); + notifySettingsChanged(oldSettings); + } + + public boolean isCaseSensitive() { + return settings.isCaseSensitive(); + } + + public void setCaseSensitive(boolean selected) { + if (settings.isCaseSensitive() == selected) { + return; + } + SearchSettings oldSettings = settings; + settings = settings.withCaseSensitive(selected); + notifySettingsChanged(oldSettings); + } + + public boolean useEscapeSequences() { + return settings.useEscapeSequences(); + } + + public void setUseEscapeSequences(boolean selected) { + if (settings.useEscapeSequences() == selected) { + return; + } + SearchSettings oldSettings = settings; + settings = settings.withUseEscapeSequence(selected); + notifySettingsChanged(oldSettings); + } + + public void setDecimalUnsigned(boolean selected) { + if (settings.isDecimalUnsigned() == selected) { + return; + } + SearchSettings oldSettings = settings; + settings = settings.withDecimalUnsigned(selected); + notifySettingsChanged(oldSettings); + } + + public boolean isDecimalUnsigned() { + return settings.isDecimalUnsigned(); + } + + public void setDecimalByteSize(int byteSize) { + if (settings.getDecimalByteSize() == byteSize) { + return; + } + SearchSettings oldSettings = settings; + settings = settings.withDecimalByteSize(byteSize); + notifySettingsChanged(oldSettings); + } + + public int getDecimalByteSize() { + return settings.getDecimalByteSize(); + } + + public void setStringCharset(Charset charset) { + if (settings.getStringCharset() == charset) { + return; + } + SearchSettings oldSettings = settings; + settings = settings.withStringCharset(charset); + notifySettingsChanged(oldSettings); + } + + public Charset getStringCharset() { + return settings.getStringCharset(); + } + + public List getMemoryRegionChoices() { + return regionChoices; + } + + public void setMatchCombiner(Combiner combiner) { + this.combiner = combiner; + } + + public Combiner getMatchCombiner() { + return combiner; + } + + public void setAutoRestrictSelection(boolean autoRestrictSelection) { + this.autoResrictSelection = autoRestrictSelection; + } + + public void selectRegion(SearchRegion region, boolean selected) { + settings.withSelectedRegion(region, selected); + } + + public boolean isSelectedRegion(SearchRegion region) { + return settings.isSelectedRegion(region); + } + + public void setSettings(SearchSettings newSettings) { + SearchSettings oldSettings = settings; + settings = newSettings; + if (!isValidRegionSettings() || this.settings.getSelectedMemoryRegions().isEmpty()) { + installDefaultRegions(); + } + notifySettingsChanged(oldSettings); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/SearchHistory.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/SearchHistory.java new file mode 100644 index 0000000000..5341dbc958 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/SearchHistory.java @@ -0,0 +1,79 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.util.*; + +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +/** + * Class for managing memory search history. It maintains a list of previously used ByteMatchers to + * do memory searching. Each ByteMatcher records the input search text and the search settings used + * to create it. + */ +public class SearchHistory { + private List history = new LinkedList<>(); + private int maxHistory; + + public SearchHistory(int maxHistory) { + this.maxHistory = maxHistory; + } + + public SearchHistory(SearchHistory other) { + this.history = new LinkedList<>(other.history); + this.maxHistory = other.maxHistory; + } + + public void addSearch(ByteMatcher matcher) { + removeSimilarMatchers(matcher); + history.addFirst(matcher); + truncateHistoryAsNeeded(); + } + + private void removeSimilarMatchers(ByteMatcher matcher) { + Iterator it = history.iterator(); + String newInput = matcher.getInput(); + SearchFormat newFormat = matcher.getSettings().getSearchFormat(); + while (it.hasNext()) { + ByteMatcher historyMatch = it.next(); + SearchFormat historyFormat = historyMatch.getSettings().getSearchFormat(); + String historyInput = historyMatch.getInput(); + if (historyFormat.equals(newFormat) && historyInput.equals(newInput)) { + it.remove(); + } + } + } + + private void truncateHistoryAsNeeded() { + int historySize = history.size(); + + if (historySize > maxHistory) { + int numToRemove = historySize - maxHistory; + + for (int i = 0; i < numToRemove; i++) { + history.remove(history.size() - 1); + } + } + } + + public ByteMatcher[] getHistoryAsArray() { + ByteMatcher[] historyArray = new ByteMatcher[history.size()]; + history.toArray(historyArray); + return historyArray; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/SearchMarkers.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/SearchMarkers.java new file mode 100644 index 0000000000..9fec961756 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/SearchMarkers.java @@ -0,0 +1,113 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.util.List; + +import javax.swing.Icon; + +import generic.theme.GIcon; +import ghidra.app.services.*; +import ghidra.app.util.SearchConstants; +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; +import ghidra.program.util.*; + +/** + * Manages the {@link MarkerSet} for a given {@link MemorySearchProvider} window. + */ +public class SearchMarkers { + private static final Icon SEARCH_MARKER_ICON = new GIcon("icon.base.search.marker"); + private MarkerService service; + private MarkerSet markerSet; + private Program program; + + public SearchMarkers(PluginTool tool, String title, Program program) { + this.program = program; + service = tool.getService(MarkerService.class); + if (service == null) { + return; + } + } + + private MarkerSet createMarkerSet(String title) { + MarkerSet markers = service.createPointMarker(title, "Search", program, + MarkerService.SEARCH_PRIORITY, true, true, false, + SearchConstants.SEARCH_HIGHLIGHT_COLOR, SEARCH_MARKER_ICON); + + markers.setMarkerDescriptor(new MarkerDescriptor() { + @Override + public ProgramLocation getProgramLocation(MarkerLocation loc) { + return new BytesFieldLocation(program, loc.getAddr()); + } + }); + + // remove it; we will add it later to a group + service.removeMarker(markers, program); + return markers; + } + + void makeActiveMarkerSet() { + if (service == null || markerSet == null) { + return; + } + service.setMarkerForGroup(MarkerService.HIGHLIGHT_GROUP, markerSet, program); + } + + void loadMarkers(String title, List matches) { + if (service == null) { + return; + } + + if (matches.isEmpty()) { + deleteMarkerSet(); + return; + } + + // If the title of the provider changes, we need to re-create the marker set as the + // provider's title is what is used as the marker set's name. The name is what shows up in + // the marker set gui for turning markers on and off - if they don't match the provider's + // title, it isn't obvious what provider the markers represent. (And currently, there is + // no way to change a marker set's name once it is created.) + if (markerSet != null && !markerSet.getName().equals(title)) { + deleteMarkerSet(); + } + + if (markerSet == null) { + markerSet = createMarkerSet(title); + } + + markerSet.clearAll(); + for (MemoryMatch match : matches) { + markerSet.add(match.getAddress()); + } + service.setMarkerForGroup(MarkerService.HIGHLIGHT_GROUP, markerSet, program); + } + + private void deleteMarkerSet() { + if (markerSet != null) { + markerSet.clearAll(); + service.removeMarker(markerSet, program); + markerSet = null; + } + } + + public void dispose() { + deleteMarkerSet(); + program = null; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/SearchSettings.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/SearchSettings.java new file mode 100644 index 0000000000..4e8083b1e9 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/SearchSettings.java @@ -0,0 +1,277 @@ +/* ### + * 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.features.base.memsearch.gui; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import ghidra.features.base.memsearch.bytesource.SearchRegion; +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.listing.Program; + +/** + * Immutable container for all the relevant search settings. + */ +public class SearchSettings { + private final SearchFormat searchFormat; + private final Set selectedRegions; + private final int alignment; + private final boolean bigEndian; + private final boolean caseSensitive; + private final boolean useEscapeSequences; + private final boolean includeInstructions; + private final boolean includeDefinedData; + private final boolean includeUndefinedData; + private final boolean isDecimalUnsigned; + private final int decimalByteSize; + private final Charset charset; + + public SearchSettings() { + this(SearchFormat.HEX, false, false, false, true, true, true, false, 4, 1, new HashSet<>(), + StandardCharsets.US_ASCII); + } + + //@formatter:off + private SearchSettings( + SearchFormat format, + boolean bigEndian, + boolean caseSensitive, + boolean useEscapeSequences, + boolean includeInstructions, + boolean includeDefinedData, + boolean includeUndefinedData, + boolean isDecimalUnsigned, + int decimalByteSize, + int alignment, + Set selectedRegions, + Charset charset) { + + this.searchFormat = format; + this.bigEndian = bigEndian; + this.caseSensitive = caseSensitive; + this.useEscapeSequences = useEscapeSequences; + this.includeInstructions = includeInstructions; + this.includeDefinedData = includeDefinedData; + this.includeUndefinedData = includeUndefinedData; + this.alignment = alignment; + this.decimalByteSize = decimalByteSize; + this.isDecimalUnsigned = isDecimalUnsigned; + this.selectedRegions = Collections.unmodifiableSet(new HashSet<>(selectedRegions)); + this.charset = charset; + + } + //@formatter:on + + /** + * Returns the {@link SearchFormat} to be used to parse the input text. + * @return the search format to be used to parse the input text + */ + public SearchFormat getSearchFormat() { + return searchFormat; + } + + /** + * Creates a copy of this settings object, but using the given search format. + * @param format the new search format + * @return a new search settings that is the same as this settings except for the format + */ + public SearchSettings withSearchFormat(SearchFormat format) { + if (this.searchFormat == format) { + return this; + } + return new SearchSettings(format, bigEndian, caseSensitive, + useEscapeSequences, includeInstructions, includeDefinedData, includeUndefinedData, + isDecimalUnsigned, decimalByteSize, alignment, selectedRegions, charset); + } + + public boolean isBigEndian() { + return bigEndian; + } + + public SearchSettings withBigEndian(boolean isBigEndian) { + if (this.bigEndian == isBigEndian) { + return this; + } + return new SearchSettings(searchFormat, isBigEndian, caseSensitive, + useEscapeSequences, includeInstructions, includeDefinedData, includeUndefinedData, + isDecimalUnsigned, decimalByteSize, alignment, selectedRegions, charset); + } + + public SearchSettings withStringCharset(Charset stringCharset) { + if (this.charset == stringCharset) { + return this; + } + return new SearchSettings(searchFormat, bigEndian, caseSensitive, + useEscapeSequences, includeInstructions, includeDefinedData, includeUndefinedData, + isDecimalUnsigned, decimalByteSize, alignment, selectedRegions, stringCharset); + } + + public Charset getStringCharset() { + return charset; + } + + public boolean useEscapeSequences() { + return useEscapeSequences; + } + + public SearchSettings withUseEscapeSequence(boolean b) { + if (this.useEscapeSequences == b) { + return this; + } + return new SearchSettings(searchFormat, bigEndian, caseSensitive, + b, includeInstructions, includeDefinedData, includeUndefinedData, + isDecimalUnsigned, decimalByteSize, alignment, selectedRegions, charset); + } + + public boolean isCaseSensitive() { + return caseSensitive; + } + + public SearchSettings withCaseSensitive(boolean b) { + if (this.caseSensitive == b) { + return this; + } + return new SearchSettings(searchFormat, bigEndian, b, + useEscapeSequences, includeInstructions, includeDefinedData, includeUndefinedData, + isDecimalUnsigned, decimalByteSize, alignment, selectedRegions, charset); + } + + public boolean isDecimalUnsigned() { + return isDecimalUnsigned; + } + + public SearchSettings withDecimalUnsigned(boolean b) { + if (this.isDecimalUnsigned == b) { + return this; + } + return new SearchSettings(searchFormat, bigEndian, caseSensitive, + useEscapeSequences, includeInstructions, includeDefinedData, includeUndefinedData, + b, decimalByteSize, alignment, selectedRegions, charset); + } + + public int getDecimalByteSize() { + return decimalByteSize; + } + + public SearchSettings withDecimalByteSize(int byteSize) { + if (this.decimalByteSize == byteSize) { + return this; + } + return new SearchSettings(searchFormat, bigEndian, caseSensitive, + useEscapeSequences, includeInstructions, includeDefinedData, includeUndefinedData, + isDecimalUnsigned, byteSize, alignment, selectedRegions, charset); + } + + public boolean includeInstructions() { + return includeInstructions; + } + + public SearchSettings withIncludeInstructions(boolean b) { + if (this.includeInstructions == b) { + return this; + } + return new SearchSettings(searchFormat, bigEndian, caseSensitive, + useEscapeSequences, b, includeDefinedData, includeUndefinedData, + isDecimalUnsigned, decimalByteSize, alignment, selectedRegions, charset); + } + + public boolean includeDefinedData() { + return includeDefinedData; + } + + public SearchSettings withIncludeDefinedData(boolean b) { + if (this.includeDefinedData == b) { + return this; + } + return new SearchSettings(searchFormat, bigEndian, caseSensitive, + useEscapeSequences, includeInstructions, b, includeUndefinedData, + isDecimalUnsigned, decimalByteSize, alignment, selectedRegions, charset); + } + + public boolean includeUndefinedData() { + return includeUndefinedData; + } + + public SearchSettings withIncludeUndefinedData(boolean b) { + if (this.includeUndefinedData == b) { + return this; + } + + return new SearchSettings(searchFormat, bigEndian, caseSensitive, + useEscapeSequences, includeInstructions, includeDefinedData, b, + isDecimalUnsigned, decimalByteSize, alignment, selectedRegions, charset); + } + + public int getAlignment() { + return alignment; + } + + public SearchSettings withAlignment(int newAlignment) { + if (this.alignment == newAlignment) { + return this; + } + return new SearchSettings(searchFormat, bigEndian, caseSensitive, + useEscapeSequences, includeInstructions, includeDefinedData, includeUndefinedData, + isDecimalUnsigned, decimalByteSize, newAlignment, selectedRegions, charset); + } + + public Set getSelectedMemoryRegions() { + return selectedRegions; + } + + public SearchSettings withSelectedRegions(Set regions) { + if (this.selectedRegions.equals(regions)) { + return this; + } + return new SearchSettings(searchFormat, bigEndian, caseSensitive, + useEscapeSequences, includeInstructions, includeDefinedData, includeUndefinedData, + isDecimalUnsigned, decimalByteSize, alignment, regions, charset); + } + + public boolean isSelectedRegion(SearchRegion region) { + return selectedRegions.contains(region); + } + + public SearchSettings withSelectedRegion(SearchRegion region, boolean select) { + return new SearchSettings(searchFormat, bigEndian, caseSensitive, + useEscapeSequences, includeInstructions, includeDefinedData, includeUndefinedData, + isDecimalUnsigned, decimalByteSize, alignment, + createRegionSet(selectedRegions, region, select), charset); + } + + public AddressSet getSearchAddresses(Program program) { + AddressSet set = new AddressSet(); + for (SearchRegion memoryRegion : selectedRegions) { + set.add(memoryRegion.getAddresses(program)); + } + return set; + } + + private static Set createRegionSet(Set regions, + SearchRegion region, boolean select) { + Set set = new HashSet<>(regions); + if (select) { + set.add(region); + } + else { + set.remove(region); + } + return set; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/ByteMatcher.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/ByteMatcher.java new file mode 100644 index 0000000000..8f167f7504 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/ByteMatcher.java @@ -0,0 +1,126 @@ +/* ### + * 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.features.base.memsearch.matcher; + +import java.util.Objects; + +import ghidra.features.base.memsearch.bytesequence.ExtendedByteSequence; +import ghidra.features.base.memsearch.gui.SearchSettings; + +/** + * ByteMatcher is the base class for an object that be used to scan bytes looking for sequences + * that match some criteria. As a convenience, it also stores the input string and settings that + * were used to generated this ByteMatcher. + */ +public abstract class ByteMatcher { + + private final String input; + private final SearchSettings settings; + + protected ByteMatcher(String input, SearchSettings settings) { + this.input = input; + this.settings = settings; + } + + /** + * Returns the original input text that generated this ByteMatacher. + * @return the original input text that generated this BytesMatcher + */ + public final String getInput() { + return input == null ? "" : input; + } + + /** + * Returns the settings used to generate this ByteMatcher. + * @return the settings used to generate this ByteMatcher + */ + public SearchSettings getSettings() { + return settings; + } + + /** + * Returns an {@link Iterable} for returning matches within the given byte sequence. + * @param bytes the byte sequence to search + * @return an iterable for return matches in the given sequence + */ + public abstract Iterable match(ExtendedByteSequence bytes); + + /** + * Returns a description of what this byte matcher matches. (Typically a sequence of bytes) + * @return a description of what this byte matcher matches + */ + public abstract String getDescription(); + + /** + * Returns additional information about this byte matcher. (Typically the mask bytes) + * @return additional information about this byte matcher + */ + public abstract String getToolTip(); + + /** + * Returns true if this byte matcher is valid and can be used to perform a search. If false, + * the the description will return a an error message explaining why this byte matcher is + * invalid. + * @return true if this byte matcher is valid and can be used to perform a search. + */ + public boolean isValidSearch() { + return true; + } + + /** + * Returns true if this byte matcher has valid (but possibly incomplete) input text. For + * example, when entering decimal values, the input could be just "-" as the user starts + * to enter a negative number. In this case the input is valid, but the {@link #isValidSearch()} + * would return false. + * @return true if this byte matcher has valid text + */ + public boolean isValidInput() { + return true; + } + + @Override + public String toString() { + return input; + } + + @Override + public int hashCode() { + return input.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + + if (getClass() != obj.getClass()) { + return false; + } + ByteMatcher other = (ByteMatcher) obj; + return Objects.equals(input, other.input) && + settings.getSearchFormat() == other.settings.getSearchFormat(); + } + + /** + * Record class to contain a match specification. + */ + public record ByteMatch(int start, int length) {} + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/InvalidByteMatcher.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/InvalidByteMatcher.java new file mode 100644 index 0000000000..ecea33ae32 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/InvalidByteMatcher.java @@ -0,0 +1,83 @@ +/* ### + * 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.features.base.memsearch.matcher; + +import org.apache.commons.collections4.iterators.EmptyIterator; + +import ghidra.features.base.memsearch.bytesequence.ExtendedByteSequence; +import ghidra.features.base.memsearch.format.SearchFormat; +import util.CollectionUtils; + +/** + * Objects of this class are the result of {@link SearchFormat}s not being able to fully parse + * input text. There are two cases. The first is the user type an illegal character for the + * selected search format. In that case this matcher is both an invalid search and an invalid + * input and the description will explain the error. The second case is the input is valid text, + * but not complete so that a fully valid byte matcher could not be created. In this case, the + * search is still invalid, but the input is valid. The description will reflect this situation. + */ +public class InvalidByteMatcher extends ByteMatcher { + + private final String errorMessage; + private final boolean isValidInput; + + /** + * Construct an invalid matcher from invalid input text. + * @param errorMessage the message describing the invalid input + */ + public InvalidByteMatcher(String errorMessage) { + this(errorMessage, false); + } + + /** + * Construct an invalid matcher from invalid input text or partial input text. + * @param errorMessage the message describing why this matcher is invalid + * @param isValidInput return true if the reason this is invalid is simply that the input + * text is not complete. For example, the user types "-" as they are starting to input + * a negative number. + */ + public InvalidByteMatcher(String errorMessage, boolean isValidInput) { + super(null, null); + this.errorMessage = errorMessage; + this.isValidInput = isValidInput; + } + + @Override + public Iterable match(ExtendedByteSequence bytes) { + return CollectionUtils.asIterable(EmptyIterator.emptyIterator()); + } + + @Override + public String getDescription() { + return errorMessage; + } + + @Override + public String getToolTip() { + return null; + } + + @Override + public boolean isValidInput() { + return isValidInput; + } + + @Override + public boolean isValidSearch() { + return false; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/MaskedByteSequenceByteMatcher.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/MaskedByteSequenceByteMatcher.java new file mode 100644 index 0000000000..0d587af996 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/MaskedByteSequenceByteMatcher.java @@ -0,0 +1,179 @@ +/* ### + * 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.features.base.memsearch.matcher; + +import java.util.Iterator; + +import org.bouncycastle.util.Arrays; + +import ghidra.features.base.memsearch.bytesequence.ByteSequence; +import ghidra.features.base.memsearch.bytesequence.ExtendedByteSequence; +import ghidra.features.base.memsearch.gui.SearchSettings; + +/** + * {@link ByteMatcher} where the user search input has been parsed into a sequence of bytes and + * masks to be used for searching a byte sequence. + */ +public class MaskedByteSequenceByteMatcher extends ByteMatcher { + + private final byte[] searchBytes; + private final byte[] masks; + + /** + * Constructor where no masking will be required. The bytes must match exactly. + * @param input the input text used to create this matcher + * @param bytes the sequence of bytes to use for searching + * @param settings the {@link SearchSettings} used to parse the input text + */ + public MaskedByteSequenceByteMatcher(String input, byte[] bytes, SearchSettings settings) { + this(input, bytes, null, settings); + } + + /** + * Constructor that includes a mask byte for each search byte. + * @param input the input text used to create this matcher + * @param bytes the sequence of bytes to use for searching + * @param masks the sequence of mask bytes to use for search. Each mask byte will be applied + * to the bytes being search before comparing them to the target bytes. + * @param settings the {@link SearchSettings} used to parse the input text + */ + public MaskedByteSequenceByteMatcher(String input, byte[] bytes, byte[] masks, + SearchSettings settings) { + super(input, settings); + + if (masks == null) { + masks = new byte[bytes.length]; + Arrays.fill(masks, (byte) 0xff); + } + + if (bytes.length != masks.length) { + throw new IllegalArgumentException("Search bytes and mask bytes must be same length!"); + } + + this.searchBytes = bytes; + this.masks = masks; + } + + @Override + public Iterable match(ExtendedByteSequence byteSequence) { + return new MatchIterator(byteSequence); + } + + @Override + public String getDescription() { + return getByteString(searchBytes); + } + + @Override + public String getToolTip() { + return "Mask = " + getByteString(masks); + } + + private String getByteString(byte[] bytes) { + StringBuilder buf = new StringBuilder(); + for (byte b : bytes) { + String hexString = Integer.toHexString(b & 0xff); + if (hexString.length() == 1) { + buf.append("0"); + } + buf.append(hexString); + buf.append(" "); + } + return buf.toString().trim(); + } + +//================================================================================================== +// Methods to facilitate testing +//================================================================================================== + public byte[] getBytes() { + return searchBytes; + } + + public byte[] getMask() { + return masks; + } + +//================================================================================================== +// Inner classes +//================================================================================================== + private class MatchIterator implements Iterator, Iterable { + + private ByteSequence byteSequence; + private int startIndex = 0; + private ByteMatch nextMatch; + + public MatchIterator(ByteSequence byteSequence) { + this.byteSequence = byteSequence; + nextMatch = findNextMatch(); + } + + @Override + public Iterator iterator() { + return this; + } + + @Override + public boolean hasNext() { + return nextMatch != null; + } + + @Override + public ByteMatch next() { + if (nextMatch == null) { + return null; + } + ByteMatch returnValue = nextMatch; + nextMatch = findNextMatch(); + return returnValue; + } + + private ByteMatch findNextMatch() { + int nextPossibleStart = findNextPossibleStart(startIndex); + while (nextPossibleStart >= 0) { + startIndex = nextPossibleStart + 1; + if (isValidMatch(nextPossibleStart)) { + return new ByteMatch(nextPossibleStart, searchBytes.length); + } + nextPossibleStart = findNextPossibleStart(startIndex); + } + return null; + } + + private boolean isValidMatch(int possibleStart) { + if (!byteSequence.hasAvailableBytes(possibleStart, searchBytes.length)) { + return false; + } + // we know 1st byte matches, check others + for (int i = 1; i < searchBytes.length; i++) { + if (searchBytes[i] != (byteSequence.getByte(possibleStart + i) & masks[i])) { + return false; + } + } + return true; + } + + private int findNextPossibleStart(int start) { + for (int i = start; i < byteSequence.getLength(); i++) { + if (searchBytes[0] == (byteSequence.getByte(i) & masks[0])) { + return i; + } + } + return -1; + } + + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/RegExByteMatcher.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/RegExByteMatcher.java new file mode 100644 index 0000000000..99f850e03f --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/RegExByteMatcher.java @@ -0,0 +1,140 @@ +/* ### + * 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.features.base.memsearch.matcher; + +import java.util.Iterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.help.UnsupportedOperationException; + +import ghidra.features.base.memsearch.bytesequence.ExtendedByteSequence; +import ghidra.features.base.memsearch.gui.SearchSettings; + +/** + * {@link ByteMatcher} where the user search input has been parsed as a regular expression. + */ +public class RegExByteMatcher extends ByteMatcher { + + private final Pattern pattern; + + public RegExByteMatcher(String input, SearchSettings settings) { + super(input, settings); + // without DOTALL mode, bytes that match line terminator characters will cause + // the regular expression pattern to not match. + this.pattern = Pattern.compile(input, Pattern.DOTALL); + } + + @Override + public Iterable match(ExtendedByteSequence byteSequence) { + return new PatternMatchIterator(byteSequence); + } + + @Override + public String getDescription() { + return "Reg Ex"; + } + + @Override + public String getToolTip() { + return null; + } + +//================================================================================================== +// Inner classes +//================================================================================================== + + /** + * Class for converting byte sequences into a {@link CharSequence} that can be used by + * the java regular expression engine + */ + private class ByteCharSequence implements CharSequence { + + private ExtendedByteSequence byteSequence; + + ByteCharSequence(ExtendedByteSequence byteSequence) { + this.byteSequence = byteSequence; + } + + @Override + public int length() { + return byteSequence.getExtendedLength(); + } + + @Override + public char charAt(int index) { + byte b = byteSequence.getByte(index); + return (char) (b & 0xff); + } + + @Override + public CharSequence subSequence(int start, int end) { + throw new UnsupportedOperationException(); + } + + } + + /** + * Adapter class for converting java {@link Pattern} matching into an iterator of + * {@link ByteMatch}s. + */ + private class PatternMatchIterator implements Iterable, Iterator { + + private Matcher matcher; + private ByteMatch nextMatch; + private ExtendedByteSequence byteSequence; + + public PatternMatchIterator(ExtendedByteSequence byteSequence) { + this.byteSequence = byteSequence; + matcher = pattern.matcher(new ByteCharSequence(byteSequence)); + nextMatch = findNextMatch(); + } + + @Override + public boolean hasNext() { + return nextMatch != null; + } + + @Override + public ByteMatch next() { + if (nextMatch == null) { + return null; + } + ByteMatch returnValue = nextMatch; + nextMatch = findNextMatch(); + return returnValue; + + } + + @Override + public Iterator iterator() { + return this; + } + + private ByteMatch findNextMatch() { + if (!matcher.find()) { + return null; + } + int start = matcher.start(); + int end = matcher.end(); + if (start >= byteSequence.getLength()) { + return null; + } + return new ByteMatch(start, end - start); + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/mask/MaskGenerator.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/mnemonic/MaskGenerator.java similarity index 99% rename from Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/mask/MaskGenerator.java rename to Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/mnemonic/MaskGenerator.java index 780a3801d7..334b765eb4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/mask/MaskGenerator.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/mnemonic/MaskGenerator.java @@ -4,16 +4,16 @@ * 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.plugin.core.searchmem.mask; +package ghidra.features.base.memsearch.mnemonic; import java.util.*; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/mask/MaskValue.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/mnemonic/MaskValue.java similarity index 97% rename from Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/mask/MaskValue.java rename to Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/mnemonic/MaskValue.java index 570593cd53..f92693a349 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/mask/MaskValue.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/mnemonic/MaskValue.java @@ -4,16 +4,16 @@ * 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.plugin.core.searchmem.mask; +package ghidra.features.base.memsearch.mnemonic; import java.util.Arrays; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/mask/MnemonicSearchPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/mnemonic/MnemonicSearchPlugin.java similarity index 95% rename from Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/mask/MnemonicSearchPlugin.java rename to Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/mnemonic/MnemonicSearchPlugin.java index 485fcef049..9bbe78bdc7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/mask/MnemonicSearchPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/mnemonic/MnemonicSearchPlugin.java @@ -4,16 +4,16 @@ * 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.plugin.core.searchmem.mask; +package ghidra.features.base.memsearch.mnemonic; import docking.action.MenuData; import ghidra.app.CorePluginPackage; @@ -23,6 +23,8 @@ import ghidra.app.events.ProgramSelectionPluginEvent; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.services.MemorySearchService; import ghidra.app.util.HelpTopics; +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.features.base.memsearch.gui.SearchSettings; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; import ghidra.program.model.listing.Program; @@ -110,13 +112,13 @@ public class MnemonicSearchPlugin extends Plugin { // dialog with the proper search string. if (mask != null) { maskedBitString = createMaskedBitString(mask.getValue(), mask.getMask()); - byte[] maskedBytes = maskedBitString.getBytes(); MemorySearchService memorySearchService = tool.getService(MemorySearchService.class); - memorySearchService.setIsMnemonic(true); - memorySearchService.search(maskedBytes, newContext); - memorySearchService.setSearchText(maskedBitString); + SearchSettings settings = new SearchSettings().withSearchFormat(SearchFormat.BINARY); + memorySearchService.createMemorySearchProvider(context.getNavigatable(), + maskedBitString, settings, false); + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/mask/SLMaskControl.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/mnemonic/SLMaskControl.java similarity index 93% rename from Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/mask/SLMaskControl.java rename to Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/mnemonic/SLMaskControl.java index 73870f9662..15c156c479 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchmem/mask/SLMaskControl.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/mnemonic/SLMaskControl.java @@ -1,20 +1,19 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * 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.plugin.core.searchmem.mask; +package ghidra.features.base.memsearch.mnemonic; /** * Represents a filter for a single instruction. This defines what portions of the instruction will diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/scan/Scanner.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/scan/Scanner.java new file mode 100644 index 0000000000..fdb823fcaa --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/scan/Scanner.java @@ -0,0 +1,71 @@ +/* ### + * 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.features.base.memsearch.scan; + +import java.util.function.Predicate; + +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.features.base.memsearch.gui.SearchSettings; +import ghidra.features.base.memsearch.matcher.ByteMatcher; +import ghidra.features.base.memsearch.searcher.MemoryMatch; + +/** + * Scan algorithms that examine the byte values of existing search results and look for changes. + * The specific scanner algorithm determines which results to keep and which to discard. + */ +public enum Scanner { + // keep unchanged results + EQUALS("Equals", mm -> compareBytes(mm) == 0, "Keep results whose values didn't change"), + // keep changed results + NOT_EQUALS("Not Equals", mm -> compareBytes(mm) != 0, "Keep results whose values changed"), + // keep results whose values increased + INCREASED("Increased", mm -> compareBytes(mm) > 0, "Keep results whose values increased"), + // keep results whose values decreased + DECREASED("Decreased", mm -> compareBytes(mm) < 0, "Keep results whose values decreased"); + + private final String name; + private final Predicate acceptCondition; + private final String description; + + private Scanner(String name, Predicate condition, String description) { + this.name = name; + this.acceptCondition = condition; + this.description = description; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public boolean accept(MemoryMatch match) { + return acceptCondition.test(match); + } + + private static int compareBytes(MemoryMatch match) { + byte[] bytes = match.getBytes(); + byte[] originalBytes = match.getPreviousBytes(); + + ByteMatcher matcher = match.getByteMatcher(); + SearchSettings settings = matcher.getSettings(); + SearchFormat searchFormat = settings.getSearchFormat(); + return searchFormat.compareValues(bytes, originalBytes, settings); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/AlignmentFilter.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/AlignmentFilter.java new file mode 100644 index 0000000000..cc0fa05804 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/AlignmentFilter.java @@ -0,0 +1,36 @@ +/* ### + * 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.features.base.memsearch.searcher; + +import java.util.function.Predicate; + +/** + * Search filter that can test a search result and determine if that result is at an address + * whose offset matches the given alignment (i.e. its offset is a multiple of the alignment value) + */ +public class AlignmentFilter implements Predicate { + + private int alignment; + + public AlignmentFilter(int alignment) { + this.alignment = alignment; + } + + @Override + public boolean test(MemoryMatch match) { + return match.getAddress().getOffset() % alignment == 0; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/CodeUnitFilter.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/CodeUnitFilter.java new file mode 100644 index 0000000000..62e5eb84cf --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/CodeUnitFilter.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.features.base.memsearch.searcher; + +import java.util.function.Predicate; + +import ghidra.program.model.listing.*; + +/** + * Search filter that can test a search result and determine if that result starts at or inside + * a code unit that matches one of the selected types. + */ +public class CodeUnitFilter implements Predicate { + + private boolean includeInstructions; + private boolean includeUndefinedData; + private boolean includeDefinedData; + private boolean includeAll; + private Listing listing; + + /** + * Constructor + * @param program the program to get code units from for testing its type + * @param includeInstructions if true, accept matches that are in an instruction + * @param includeDefinedData if true, accept matches that are in defined data + * @param includeUndefinedData if true, accept matches that are in undefined data + */ + public CodeUnitFilter(Program program, boolean includeInstructions, boolean includeDefinedData, + boolean includeUndefinedData) { + this.listing = program.getListing(); + this.includeInstructions = includeInstructions; + this.includeDefinedData = includeDefinedData; + this.includeUndefinedData = includeUndefinedData; + this.includeAll = includeInstructions && includeDefinedData && includeUndefinedData; + } + + @Override + public boolean test(MemoryMatch match) { + if (includeAll) { + return true; + } + CodeUnit codeUnit = listing.getCodeUnitContaining(match.getAddress()); + if (codeUnit instanceof Instruction) { + return includeInstructions; + } + else if (codeUnit instanceof Data) { + Data data = (Data) codeUnit; + if (data.isDefined()) { + return includeDefinedData; + } + return includeUndefinedData; + } + return false; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/MemoryMatch.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/MemoryMatch.java new file mode 100644 index 0000000000..366ed32cda --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/MemoryMatch.java @@ -0,0 +1,114 @@ +/* ### + * 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.features.base.memsearch.searcher; + +import java.util.Arrays; +import java.util.Objects; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; +import ghidra.program.model.address.Address; + +/** + * A class that represents a memory search hit at an address. Matches can also be updated with + * new byte values (from a scan or refresh action). The original bytes that matched the original + * search are maintained in addition to the "refreshed" bytes. + */ +public class MemoryMatch implements Comparable { + + private final Address address; + private byte[] bytes; + private byte[] previousBytes; + private final ByteMatcher matcher; + + public MemoryMatch(Address address, byte[] bytes, ByteMatcher matcher) { + if (bytes == null || bytes.length < 1) { + throw new IllegalArgumentException("Must provide at least 1 byte"); + } + this.address = Objects.requireNonNull(address); + this.bytes = bytes; + this.previousBytes = bytes; + this.matcher = matcher; + } + + public MemoryMatch(Address address) { + this.address = address; + this.matcher = null; + } + + public void updateBytes(byte[] newBytes) { + previousBytes = bytes; + if (!Arrays.equals(bytes, newBytes)) { + bytes = newBytes; + } + } + + public Address getAddress() { + return address; + } + + public int getLength() { + return bytes.length; + } + + public byte[] getBytes() { + return bytes; + } + + public byte[] getPreviousBytes() { + return previousBytes; + } + + public ByteMatcher getByteMatcher() { + return matcher; + } + + @Override + public int compareTo(MemoryMatch o) { + return address.compareTo(o.address); + } + + @Override + public int hashCode() { + return address.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + MemoryMatch other = (MemoryMatch) obj; + // just compare addresses. The bytes are mutable and we want matches to be equal even + // if the bytes are different + return Objects.equals(address, other.address); + } + + @Override + public String toString() { + return address.toString(); + } + + public boolean isChanged() { + return !bytes.equals(previousBytes); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/MemorySearcher.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/MemorySearcher.java new file mode 100644 index 0000000000..3985d83687 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/MemorySearcher.java @@ -0,0 +1,337 @@ +/* ### + * 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.features.base.memsearch.searcher; + +import java.util.function.Predicate; + +import ghidra.features.base.memsearch.bytesequence.*; +import ghidra.features.base.memsearch.bytesource.AddressableByteSource; +import ghidra.features.base.memsearch.matcher.ByteMatcher; +import ghidra.features.base.memsearch.matcher.ByteMatcher.ByteMatch; +import ghidra.program.model.address.*; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.task.TaskMonitor; + +/** + * Class for searching bytes from a byteSource (memory) using a {@link ByteMatcher}. It handles + * breaking the search down into a series of searches, handling gaps in the address set and + * breaking large address ranges down into manageable sizes. + *

+ * It is created with a specific byte source, matcher, address set, and search limit. Clients can + * then either call the {@link #findAll(Accumulator, TaskMonitor)} method or use it to incrementally + * search using {@link #findNext(Address, TaskMonitor)}, + * {@link #findPrevious(Address, TaskMonitor)}, or {@link #findOnce(Address, boolean, TaskMonitor)}. + */ + +public class MemorySearcher { + private static final int DEFAULT_CHUNK_SIZE = 16 * 1024; + private static final int OVERLAP_SIZE = 100; + private final AddressableByteSequence bytes1; + private final AddressableByteSequence bytes2; + private final ByteMatcher matcher; + private final int chunkSize; + + private Predicate filter = r -> true; + private final int searchLimit; + private final AddressSetView searchSet; + + /** + * Constructor + * @param byteSource the source of the bytes to be searched + * @param matcher the matcher that can find matches in a byte sequence + * @param addresses the address in the byte source to search + * @param searchLimit the max number of hits before stopping + */ + public MemorySearcher(AddressableByteSource byteSource, ByteMatcher matcher, + AddressSet addresses, int searchLimit) { + this(byteSource, matcher, addresses, searchLimit, DEFAULT_CHUNK_SIZE); + } + + /** + * Constructor + * @param byteSource the source of the bytes to be searched + * @param matcher the matcher that can find matches in a byte sequence + * @param addresses the address in the byte source to search + * @param searchLimit the max number of hits before stopping + * @param chunkSize the maximum number of bytes to feed to the matcher at any one time. + */ + public MemorySearcher(AddressableByteSource byteSource, ByteMatcher matcher, + AddressSet addresses, int searchLimit, int chunkSize) { + this.matcher = matcher; + this.searchSet = addresses; + this.searchLimit = searchLimit; + this.chunkSize = chunkSize; + + bytes1 = new AddressableByteSequence(byteSource, chunkSize); + bytes2 = new AddressableByteSequence(byteSource, chunkSize); + } + + /** + * Sets any match filters. The filter can be used to exclude matches that don't meet some + * criteria that is not captured in the byte matcher such as alignment and code unit type. + * @param filter the predicate to use to filter search results + */ + public void setMatchFilter(Predicate filter) { + this.filter = filter; + } + + /** + * Searches all the addresses in this search's {@link AddressSetView} using the byte matcher to + * find matches. As each match is found (and passes any filters), the match is given to the + * accumulator. The search continues until either the entire address set has been search or + * the search limit has been reached. + * @param accumulator the accumulator for found matches + * @param monitor the task monitor + * @return true if the search completed searching through the entire address set. + */ + public boolean findAll(Accumulator accumulator, TaskMonitor monitor) { + monitor.initialize(searchSet.getNumAddresses(), "Searching..."); + + for (AddressRange range : searchSet.getAddressRanges()) { + if (!findAll(accumulator, range, monitor)) { + return false; + } + } + return true; + } + + /** + * Searches forwards or backwards starting at the given address until a match is found or + * the start or end of the address set is reached. It does not currently wrap the search. + * @param start the address to start searching + * @param forward if true, search forward, otherwise, search backwards. + * @param monitor the task monitor + * @return the first match found or null if no match found. + */ + public MemoryMatch findOnce(Address start, boolean forward, TaskMonitor monitor) { + if (forward) { + return findNext(start, monitor); + } + return findPrevious(start, monitor); + } + + /** + * Searches forwards starting at the given address until a match is found or + * the end of the address set is reached. It does not currently wrap the search. + * @param start the address to start searching + * @param monitor the task monitor + * @return the first match found or null if no match found. + */ + public MemoryMatch findNext(Address start, TaskMonitor monitor) { + + long numAddresses = searchSet.getNumAddresses() - searchSet.getAddressCountBefore(start); + monitor.initialize(numAddresses, "Searching...."); + + for (AddressRange range : searchSet.getAddressRanges(start, true)) { + range = range.intersectRange(start, range.getMaxAddress()); + MemoryMatch match = findFirst(range, monitor); + if (match != null) { + return match; + } + if (monitor.isCancelled()) { + break; + } + } + return null; + } + + /** + * Searches backwards starting at the given address until a match is found or + * the beginning of the address set is reached. It does not currently wrap the search. + * @param start the address to start searching + * @param monitor the task monitor + * @return the first match found or null if no match found. + */ + public MemoryMatch findPrevious(Address start, TaskMonitor monitor) { + + monitor.initialize(searchSet.getAddressCountBefore(start) + 1, "Searching...."); + + for (AddressRange range : searchSet.getAddressRanges(start, false)) { + MemoryMatch match = findLast(range, start, monitor); + if (match != null) { + return match; + } + if (monitor.isCancelled()) { + break; + } + } + return null; + } + + private MemoryMatch findFirst(AddressRange range, TaskMonitor monitor) { + AddressableByteSequence searchBytes = bytes1; + AddressableByteSequence extra = bytes2; + + AddressRangeIterator it = new AddressRangeSplitter(range, chunkSize, true); + AddressRange first = it.next(); + + searchBytes.setRange(first); + while (it.hasNext()) { + AddressRange next = it.next(); + extra.setRange(next); + + MemoryMatch match = findFirst(searchBytes, extra, monitor); + if (match != null) { + return match; + } + if (monitor.isCancelled()) { + break; + } + + // Flip flop the byte buffers, making the extended buffer become primary and preparing + // the primary buffer to be used to read the next chunk. See the + // ExtendedByteSequence class for an explanation of this approach. + searchBytes = extra; + extra = searchBytes == bytes1 ? bytes2 : bytes1; + } + // last segment, no extra bytes to overlap, so just search the primary buffer + extra.clear(); + return findFirst(searchBytes, extra, monitor); + } + + private MemoryMatch findLast(AddressRange range, Address start, TaskMonitor monitor) { + AddressableByteSequence searchBytes = bytes1; + AddressableByteSequence extra = bytes2; + extra.clear(); + + if (range.contains(start)) { + Address min = range.getMinAddress(); + Address max = range.getMaxAddress(); + range = new AddressRangeImpl(min, start); + AddressRange remaining = new AddressRangeImpl(start.next(), max); + AddressRange extraRange = new AddressRangeSplitter(remaining, chunkSize, true).next(); + extra.setRange(extraRange); + } + + AddressRangeIterator it = new AddressRangeSplitter(range, chunkSize, false); + + while (it.hasNext()) { + AddressRange next = it.next(); + searchBytes.setRange(next); + MemoryMatch match = findLast(searchBytes, extra, monitor); + if (match != null) { + return match; + } + if (monitor.isCancelled()) { + break; + } + + // Flip flop the byte buffers, making the primary buffer the new extended buffer + // and refilling the primary buffer with new data going backwards. + extra = searchBytes; + searchBytes = extra == bytes1 ? bytes2 : bytes1; + } + return null; + } + + private MemoryMatch findFirst(AddressableByteSequence searchBytes, ByteSequence extra, + TaskMonitor monitor) { + + ExtendedByteSequence searchSequence = + new ExtendedByteSequence(searchBytes, extra, OVERLAP_SIZE); + + for (ByteMatch byteMatch : matcher.match(searchSequence)) { + Address address = searchBytes.getAddress(byteMatch.start()); + byte[] bytes = searchSequence.getBytes(byteMatch.start(), byteMatch.length()); + MemoryMatch match = new MemoryMatch(address, bytes, matcher); + if (filter.test(match)) { + return match; + } + if (monitor.isCancelled()) { + break; + } + } + monitor.incrementProgress(searchBytes.getLength()); + return null; + } + + private MemoryMatch findLast(AddressableByteSequence searchBytes, ByteSequence extra, + TaskMonitor monitor) { + + MemoryMatch last = null; + + ExtendedByteSequence searchSequence = + new ExtendedByteSequence(searchBytes, extra, OVERLAP_SIZE); + + for (ByteMatch byteMatch : matcher.match(searchSequence)) { + Address address = searchBytes.getAddress(byteMatch.start()); + byte[] bytes = searchSequence.getBytes(byteMatch.start(), byteMatch.length()); + MemoryMatch match = new MemoryMatch(address, bytes, matcher); + if (filter.test(match)) { + last = match; + } + if (monitor.isCancelled()) { + return null; + } + } + monitor.incrementProgress(searchBytes.getLength()); + return last; + } + + private boolean findAll(Accumulator accumulator, AddressRange range, + TaskMonitor monitor) { + AddressableByteSequence searchBytes = bytes1; + AddressableByteSequence extra = bytes2; + + AddressRangeIterator it = new AddressRangeSplitter(range, chunkSize, true); + AddressRange first = it.next(); + + searchBytes.setRange(first); + while (it.hasNext()) { + AddressRange next = it.next(); + extra.setRange(next); + if (!findAll(accumulator, searchBytes, extra, monitor)) { + return false; + } + searchBytes = extra; + extra = searchBytes == bytes1 ? bytes2 : bytes1; + } + extra.clear(); + return findAll(accumulator, searchBytes, extra, monitor); + } + + private boolean findAll(Accumulator accumulator, + AddressableByteSequence searchBytes, ByteSequence extra, TaskMonitor monitor) { + + if (monitor.isCancelled()) { + return false; + } + + ExtendedByteSequence searchSequence = + new ExtendedByteSequence(searchBytes, extra, OVERLAP_SIZE); + + for (ByteMatch byteMatch : matcher.match(searchSequence)) { + Address address = searchBytes.getAddress(byteMatch.start()); + byte[] bytes = searchSequence.getBytes(byteMatch.start(), byteMatch.length()); + MemoryMatch match = new MemoryMatch(address, bytes, matcher); + if (filter.test(match)) { + if (accumulator.size() >= searchLimit) { + return false; + } + accumulator.add(match); + } + if (monitor.isCancelled()) { + return false; + } + + } + // Reset the monitor message, since clients may change the message (such as the + // incremental table loader) + monitor.setMessage("Searching..."); + monitor.incrementProgress(searchBytes.getLength()); + return true; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/search/memory/MemSearchResult.java b/Ghidra/Features/Base/src/main/java/ghidra/util/search/memory/MemSearchResult.java index fafd536d2f..2294301292 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/util/search/memory/MemSearchResult.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/search/memory/MemSearchResult.java @@ -27,6 +27,7 @@ public class MemSearchResult implements Comparable { private Address address; private int length; + private byte[] bytes; public MemSearchResult(Address address, int length) { this.address = Objects.requireNonNull(address); @@ -37,6 +38,15 @@ public class MemSearchResult implements Comparable { this.length = length; } + public MemSearchResult(Address address, byte[] bytes) { + if (bytes == null || bytes.length < 1) { + throw new IllegalArgumentException("Must provide at least 1 byte"); + } + this.address = Objects.requireNonNull(address); + this.bytes = bytes; + this.length = bytes.length; + } + public Address getAddress() { return address; } @@ -45,6 +55,10 @@ public class MemSearchResult implements Comparable { return length; } + public byte[] getBytes() { + return bytes; + } + @Override public int compareTo(MemSearchResult o) { return address.compareTo(o.address); diff --git a/Ghidra/Features/Base/src/main/resources/images/view_bottom.png b/Ghidra/Features/Base/src/main/resources/images/view_bottom.png new file mode 100644 index 0000000000..e883ed1206 Binary files /dev/null and b/Ghidra/Features/Base/src/main/resources/images/view_bottom.png differ diff --git a/Ghidra/Features/Base/src/main/resources/images/view_left_right.png b/Ghidra/Features/Base/src/main/resources/images/view_left_right.png new file mode 100644 index 0000000000..a5748383f3 Binary files /dev/null and b/Ghidra/Features/Base/src/main/resources/images/view_left_right.png differ diff --git a/Ghidra/Features/Base/src/main/resources/images/view_top_bottom.png b/Ghidra/Features/Base/src/main/resources/images/view_top_bottom.png new file mode 100644 index 0000000000..cdd2d2c967 Binary files /dev/null and b/Ghidra/Features/Base/src/main/resources/images/view_top_bottom.png differ diff --git a/Ghidra/Features/Base/src/main/resources/images/viewmag+.png b/Ghidra/Features/Base/src/main/resources/images/viewmag+.png new file mode 100644 index 0000000000..902b292a0a Binary files /dev/null and b/Ghidra/Features/Base/src/main/resources/images/viewmag+.png differ diff --git a/Ghidra/Features/Base/src/main/resources/images/viewmag.png b/Ghidra/Features/Base/src/main/resources/images/viewmag.png new file mode 100644 index 0000000000..6dd1931589 Binary files /dev/null and b/Ghidra/Features/Base/src/main/resources/images/viewmag.png differ diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/AbstractMemSearchTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/AbstractMemSearchTest.java deleted file mode 100644 index ce118b9ab5..0000000000 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/AbstractMemSearchTest.java +++ /dev/null @@ -1,307 +0,0 @@ -/* ### - * 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.plugin.core.searchmem; - -import static org.junit.Assert.*; - -import java.awt.Container; -import java.awt.Window; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import javax.swing.*; - -import org.apache.commons.collections4.IteratorUtils; - -import docking.action.DockingActionIf; -import docking.test.AbstractDockingTest; -import docking.widgets.fieldpanel.support.Highlight; -import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; -import ghidra.app.plugin.core.codebrowser.CodeViewerProvider; -import ghidra.app.plugin.core.table.TableComponentProvider; -import ghidra.app.plugin.core.table.TableServicePlugin; -import ghidra.app.services.*; -import ghidra.app.util.ListingHighlightProvider; -import ghidra.app.util.viewer.field.BytesFieldFactory; -import ghidra.app.util.viewer.field.ListingField; -import ghidra.app.util.viewer.format.FormatManager; -import ghidra.program.model.address.*; -import ghidra.program.model.listing.*; -import ghidra.program.model.mem.Memory; -import ghidra.test.AbstractProgramBasedTest; -import ghidra.util.Msg; -import ghidra.util.search.memory.MemSearchResult; -import ghidra.util.table.GhidraTable; - -/** - * Base class for memory search tests. - */ -public abstract class AbstractMemSearchTest extends AbstractProgramBasedTest { - - protected MemSearchPlugin memSearchPlugin; - protected DockingActionIf searchAction; - protected CodeBrowserPlugin cb; - protected CodeViewerProvider provider; - protected JLabel statusLabel; - protected JTextField valueField; - protected JComboBox valueComboBox; - protected Container pane; - protected JLabel hexLabel; - protected Memory memory; - protected Listing listing; - protected TableServicePlugin tableServicePlugin; - protected MarkerService markerService; - - protected MemSearchDialog dialog; - - /* - * Note that this setup function does not have the @Before annotation - this is because - * sub-classes often need to override this and if we have the annotation here, the test - * runner will only invoke this base class implementation. - */ - public void setUp() throws Exception { - - // this builds the program and launches the tool - initialize(); - - memSearchPlugin = env.getPlugin(MemSearchPlugin.class); - - listing = program.getListing(); - memory = program.getMemory(); - - searchAction = getAction(memSearchPlugin, "Search Memory"); - - cb = codeBrowser; // TODO delete after 7.3 release; just use the parent's CodeBrowser - - provider = cb.getProvider(); - markerService = tool.getService(MarkerService.class); - - tableServicePlugin = env.getPlugin(TableServicePlugin.class); - - showMemSearchDialog(); - setToggleButtonSelected(pane, MemSearchDialog.ADVANCED_BUTTON_NAME, true); - selectRadioButton("Binary"); - } - - @Override - protected Program getProgram() throws Exception { - return buildProgram(); - } - - protected abstract Program buildProgram() throws Exception; - - protected void waitForSearch(String panelName, int expectedResults) { - - waitForCondition(() -> { - return !memSearchPlugin.isSearching(); - }, "Timed-out waiting for search results"); - - Window window = AbstractDockingTest.waitForWindowByTitleContaining(panelName); - GhidraTable gTable = findComponent(window, GhidraTable.class, true); - waitForSwing(); - assertEquals(expectedResults, gTable.getRowCount()); - } - - protected void waitForSearchTask() { - waitForSwing(); - Thread t = dialog.getTaskScheduler().getCurrentThread(); - if (t == null) { - return; - } - - try { - t.join(); - } - catch (InterruptedException e) { - Msg.debug(this, "Interrupted waiting for the search task thread to finish"); - } - waitForSwing(); - } - - protected void showMemSearchDialog() { - performAction(searchAction, provider, true); - // dig up the components of the dialog - dialog = waitForDialogComponent(MemSearchDialog.class); - pane = dialog.getComponent(); - - statusLabel = (JLabel) findComponentByName(pane, "statusLabel"); - valueComboBox = findComponent(pane, JComboBox.class); - valueField = (JTextField) valueComboBox.getEditor().getEditorComponent(); - hexLabel = (JLabel) findComponentByName(pane, "HexSequenceField"); - } - - protected void selectRadioButton(String text) { - setToggleButtonSelected(pane, text, true); - } - - protected void selectCheckBox(String text, boolean state) { - setToggleButtonSelected(pane, text, state); - } - - @SuppressWarnings("unchecked") - private List

getHighlightAddresses() { - CodeViewerService service = tool.getService(CodeViewerService.class); - Object codeViewerProvider = getInstanceField("connectedProvider", service); - Map highlighterMap = - (Map) getInstanceField("programHighlighterMap", - codeViewerProvider); - ListingHighlightProvider highlightProvider = highlighterMap.get(program); - - assertEquals("The inner-class has been renamed", "SearchTableHighlightHandler", - highlightProvider.getClass().getSimpleName()); - - MemSearchTableModel model = - (MemSearchTableModel) getInstanceField("model", highlightProvider); - List data = model.getModelData(); - return data.stream().map(result -> result.getAddress()).collect(Collectors.toList()); - } - - protected void checkMarkerSet(List
expected) { - - TableComponentProvider[] providers = tableServicePlugin.getManagedComponents(); - TableComponentProvider tableProvider = providers[0]; - assertTrue(tool.isVisible(tableProvider)); - - List
highlights = getHighlightAddresses(); - assertListEqualUnordered("Search highlights not correctly generated", expected, highlights); - - MarkerSet markers = - runSwing(() -> markerService.getMarkerSet(tableProvider.getName(), program)); - assertNotNull(markers); - - AddressSet addressSet = runSwing(() -> markers.getAddressSet()); - AddressIterator it = addressSet.getAddresses(true); - List
list = IteratorUtils.toList(it); - - assertListEqualUnordered("Search markers not correctly generated", expected, list); - } - - protected void pressSearchAllButton() { - runSwing(() -> invokeInstanceMethod("allCallback", dialog)); - } - - protected void pressSearchButton(String text) throws Exception { - pressButtonByText(pane, text); - waitForSearchTask(); - } - - protected void performSearchTest(List
expected, String buttonText) throws Exception { - - for (Address addr : expected) { - pressSearchButton(buttonText); - assertEquals("Found", getStatusText()); - cb.updateNow(); - assertEquals(addr, cb.getCurrentLocation().getAddress()); - } - - pressSearchButton(buttonText); - assertEquals("Not Found", getStatusText()); - } - - protected String getStatusText() { - AtomicReference ref = new AtomicReference<>(); - runSwing(() -> ref.set(statusLabel.getText())); - return ref.get(); - } - - protected void setValueText(String s) { - setText(valueField, s); - } - - protected void myTypeText(String text) { - // Note: we do not use setFocusedComponent(valueField), as that method will fail if the - // focus change doesn't work. Here, we will keep on going if the focus change - // doesn't work. - runSwing(() -> valueField.requestFocus()); - triggerText(valueField, text); - } - - protected ListingHighlightProvider getHighlightProvider() { - CodeViewerService service = tool.getService(CodeViewerService.class); - FormatManager fm = (FormatManager) getInstanceField("formatMgr", service); - return (ListingHighlightProvider) getInstanceField("highlightProvider", fm); - } - - protected void repeatSearch() { - DockingActionIf action = getAction(memSearchPlugin, "Repeat Memory Search"); - assertTrue(action.isEnabled()); - performAction(action, provider, true); - waitForSearchTask(); - } - - protected Address currentAddress() { - cb.updateNow(); - Address addr = cb.getCurrentLocation().getAddress(); - return addr; - } - - protected CodeUnit currentCodeUnit() { - CodeUnit cu = program.getListing().getCodeUnitContaining(currentAddress()); - return cu; - } - - protected CodeUnit codeUnitContaining(Address addr) { - CodeUnit cu = program.getListing().getCodeUnitContaining(addr); - return cu; - } - - protected void assertSearchSelectionSelected() { - - AbstractButton b = findAbstractButtonByText(pane, "Search Selection"); - assertTrue(isEnabled(b)); - assertTrue(isSelected(b)); - } - - protected void assertButtonState(String text, boolean isEnabled, boolean isSelected) { - - AbstractButton b = findAbstractButtonByText(pane, text); - assertEquals(isEnabled, isEnabled(b)); - assertEquals(isSelected, isSelected(b)); - } - - protected void assertEnabled(String text, boolean isEnabled) { - // Note: we do not use the findAbstractButtonByText() here as there are two buttons with - // the same text. Only one of the buttons is actually a JButton, so this call works. - // Ideally, all buttons would have a name set so that wouldn't have to rely on the - // button text. - JButton b = findButtonByText(pane, text); - assertEquals(isEnabled, isEnabled(b)); - } - - protected void setAlignment(String alignment) { - JTextField alignmentField = - (JTextField) findComponentByName(dialog.getComponent(), "Alignment"); - setText(alignmentField, alignment); - } - - protected Highlight[] getByteHighlights(Address address, String bytes) { - goTo(address); - CodeUnit cu = codeUnitContaining(address); - ListingHighlightProvider hlProvider = getHighlightProvider(); - ListingField field = getField(address, BytesFieldFactory.FIELD_NAME); - return hlProvider.createHighlights(bytes, field, -1); - } - - protected void setEndianness(String text) { - // we use this method because the given button may be disabled, which means we cannot - // click it, but we can select it - AbstractButton button = findAbstractButtonByText(pane, text); - runSwing(() -> button.setSelected(true)); - } - -} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchAsciiTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchAsciiTest.java deleted file mode 100644 index ef2300365a..0000000000 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchAsciiTest.java +++ /dev/null @@ -1,433 +0,0 @@ -/* ### - * 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.plugin.core.searchmem; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.List; - -import javax.swing.JComboBox; - -import org.junit.Before; -import org.junit.Test; - -import ghidra.program.database.ProgramBuilder; -import ghidra.program.model.address.Address; -import ghidra.program.model.listing.Program; -import ghidra.test.ToyProgramBuilder; - -/** - * Tests for searching memory for ascii. - */ -public class MemSearchAsciiTest extends AbstractMemSearchTest { - - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - selectRadioButton("String"); - } - - @Override - protected Program buildProgram() throws Exception { - ToyProgramBuilder builder = new ToyProgramBuilder("Test", false, ProgramBuilder._TOY); - - builder.createMemory(".text", "0x1001000", 0x6600); - builder.createMemory(".data", "0x1008000", 0x600); - builder.createMemory(".rsrc", "0x100a000", 0x5400); - //create some strings - builder.createEncodedString("0x010016ec", "something", StandardCharsets.UTF_16LE, true); - builder.createEncodedString("0x01001708", "Notepad", StandardCharsets.UTF_16LE, true); - builder.createEncodedString("0x01001740", "something else", StandardCharsets.UTF_16LE, - true); - builder.createEncodedString("0x01001840", "\u039d\u03bf\u03c4\u03b5\u03c0\u03b1\u03bd", - StandardCharsets.UTF_16LE, true); - builder.createEncodedString("0x0100186a", - "\u03c1\u03b8\u03c4\u03b5\u03c0\u03b1\u03bd\u03c2\u03b2", StandardCharsets.UTF_16LE, - true); - builder.createEncodedString("0x0100196a", - "\u03c1\u03b8\u03c4\u03b5\u03c0\u03b1\u03bd\u03c2\u03b2", StandardCharsets.UTF_8, true); - builder.createEncodedString("0x0100189d", "\"Hello world!\"\n\t-new programmer", - StandardCharsets.US_ASCII, true); - builder.createEncodedString("0x0100198e", "\"Hello world!\"\n\t-new programmer", - StandardCharsets.UTF_16LE, true); - builder.createEncodedString("0x010013cc", "notepad.exe", StandardCharsets.US_ASCII, false); - builder.createEncodedString("0x010013e0", "notepad.exe", StandardCharsets.US_ASCII, false); - builder.createEncodedString("0x1006c6a", "GetLocaleInfoW", StandardCharsets.US_ASCII, - false); - builder.createEncodedString("0x1006f26", "GetCPInfo", StandardCharsets.US_ASCII, false); - builder.createEncodedString("0x0100dde0", "NOTEPAD.EXE", StandardCharsets.UTF_16LE, true); - builder.createEncodedString("0x0100eb90", - "This string contains notepad twice. Here is the second NotePad.", - StandardCharsets.UTF_16LE, true); - builder.createEncodedString("0x0100ed00", "Another string", StandardCharsets.UTF_16LE, - true); - - return builder.getProgram(); - } - - @Test - public void testStringFormatSelected() throws Exception { - // verify that String options are showing: case sensitive, unicode, - // regular expression check boxes. - assertButtonState("Case Sensitive", true, false); - - @SuppressWarnings("unchecked") - JComboBox comboBox = - (JComboBox) findComponentByName(pane, "Encoding Options"); - assertNotNull(comboBox); - } - - @Test - public void testCaseSensitiveOff() throws Exception { - - selectCheckBox("Case Sensitive", false); - - setValueText("notepad"); - - List
addrs = addrs(0x010013cc, 0x010013e0); - - performSearchTest(addrs, "Next"); - } - - @Test - public void testCaseSensitiveOn() throws Exception { - - selectCheckBox("Case Sensitive", true); - - setValueText("NOTEpad"); - - pressSearchButton("Next"); - - assertEquals("Not Found", statusLabel.getText()); - } - - @Test - public void testUnicodeNotCaseSensitive() throws Exception { - - selectCheckBox("Case Sensitive", false); - - setEncoding(StandardCharsets.UTF_16); - setValueText("NOTEpad"); - - List
addrs = addrs(0x01001708, 0x0100dde0, 0x0100eb90, 0x0100eb90); // this code unit contains two notepads in one string - - performSearchTest(addrs, "Next"); - } - - @Test - public void testGreekUnicodeSearch() throws Exception { - selectCheckBox("Case Sensitive", false); - - setEncoding(StandardCharsets.UTF_16); - setValueText("\u03c4\u03b5\u03c0\u03b1\u03bd"); - - List
addrs = addrs(0x01001840, 0x0100186a); - - performSearchTest(addrs, "Next"); - - addrs.add(addr(0x0100196a)); - - setEncoding(StandardCharsets.UTF_8); - pressSearchButton("Next"); - assertEquals("Found", statusLabel.getText()); - assertEquals(addrs.get(2), cb.getCurrentLocation().getAddress()); - - pressSearchButton("Next"); - assertEquals("Not Found", statusLabel.getText()); - } - - @Test - public void testRepeatUnicodeNotCaseSensitive() throws Exception { - selectCheckBox("Case Sensitive", false); - - setEncoding(StandardCharsets.UTF_16); - setValueText("NOTEpad"); - - List
startList = addrs(0x01001708, 0x0100dde0, 0x0100eb90, 0x0100eb90); // this code unit contains two notepads in one string - - for (int i = 0; i < startList.size(); i++) { - Address start = startList.get(i); - if (i == 0) { - pressSearchButton("Next"); - } - else { - repeatSearch(); - } - assertEquals(start, cb.getCurrentLocation().getAddress()); - assertEquals("Found", statusLabel.getText()); - } - pressSearchButton("Next"); - assertEquals("Not Found", statusLabel.getText()); - } - - @Test - public void testUnicodeCaseSensitive() throws Exception { - selectCheckBox("Case Sensitive", true); - - setEncoding(StandardCharsets.UTF_16); - setValueText("Notepad"); - - performSearchTest(addrs(0x01001708), "Next"); - } - - @Test - public void testUnicodeBigEndian() throws Exception { - - // with Big Endian selected, unicode bytes should be reversed - setEndianness("Big Endian"); - - setEncoding(StandardCharsets.UTF_16); - setValueText("start"); - - assertEquals("00 73 00 74 00 61 00 72 00 74 ", hexLabel.getText()); - - selectRadioButton("Little Endian"); - assertEquals("73 00 74 00 61 00 72 00 74 00 ", hexLabel.getText()); - } - - @Test - public void testSearchAllUnicodeNotCaseSensitive() throws Exception { - // test for markers - - // QueryResults should get displayed - // test the marker stuff - selectCheckBox("Case Sensitive", false); - - setEncoding(StandardCharsets.UTF_16); - setValueText("NOTEpad"); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 4); - - List
addrs = addrs(0x01001708, 0x0100dde0, 0x0100ebba, 0x0100ebfe); - - checkMarkerSet(addrs); - } - - @Test - public void testSearchAllUnicodeCaseSensitive() throws Exception { - // test for markers - - // QueryResults should get displayed - // test the marker stuff - selectCheckBox("Case Sensitive", true); - - setEncoding(StandardCharsets.UTF_16); - setValueText("Notepad"); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 1); - - checkMarkerSet(addrs(0x01001708)); - } - - @Test - public void testSearchAllNotCaseSensitive() throws Exception { - // QueryResults should get displayed - // test the marker stuff - selectCheckBox("Case Sensitive", false); - - setValueText("NOTEpad"); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 2); - - List
addrs = addrs(0x010013cc, 0x010013e0); - - checkMarkerSet(addrs); - } - - @Test - public void testSearchAllCaseSensitive() throws Exception { - // QueryResults should get displayed - // test the marker stuff - // create an set of ascii bytes to do this test - byte[] b = new byte[] { 'N', 'O', 'T', 'E', 'p', 'a', 'd' }; - - int transactionID = program.startTransaction("test"); - memory.setBytes(addr(0x0100b451), b); - program.endTransaction(transactionID, true); - - selectCheckBox("Case Sensitive", true); - - setValueText("NOTEpad"); - pressSearchAllButton(); - - waitForSearch("Search Memory - ", 1); - - checkMarkerSet(addrs(0x0100b451)); - } - - @Test - public void testSearchAllCaseSensitiveAlign8() throws Exception { - // QueryResults should get displayed - // test the marker stuff - // create an set of ascii bytes to do this test - - setAlignment("8"); - - selectCheckBox("Case Sensitive", true); - - setValueText("notepad"); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 1); - - checkMarkerSet(addrs(0x010013e0)); - } - - @Test - public void testSearchSelection() throws Exception { - - makeSelection(tool, program, addr(0x01006c73), addr(0x01006f02)); - - assertSearchSelectionSelected(); - - selectCheckBox("Case Sensitive", false); - - setValueText("Info"); - - performSearchTest(addrs(0x01006c6a), "Next"); - } - - @Test - public void testSearchNonContiguousSelection() throws Exception { - - makeSelection(tool, program, range(0x01006c70, 0x01006c80), range(0x01006f2b, 0x01006f37)); - - assertSearchSelectionSelected(); - - selectCheckBox("Case Sensitive", false); - - setValueText("Info"); - - List
addrs = addrs(0x01006c6a, 0x01006f26); - - performSearchTest(addrs, "Next"); - } - - @Test - public void testSearchBackward() throws Exception { - - goTo(tool, program, addr(0x1006f56)); - - selectCheckBox("Case Sensitive", true); - - setValueText("Info"); - - List
addrs = addrs(0x01006f26, 0x01006c6a); - - performSearchTest(addrs, "Previous"); - } - - @Test - public void testSearchBackwardInSelection() throws Exception { - - goTo(tool, program, addr(0x01006f02)); - - makeSelection(tool, program, addr(0x01006c73), addr(0x01006f02)); - - assertSearchSelectionSelected(); - - selectCheckBox("Case Sensitive", false); - - setValueText("Info"); - - List
addrs = addrs(0x01006c6a); - - performSearchTest(addrs, "Previous"); - } - - @Test - public void testSearchBackwardAlign4() throws Exception { - - goTo(tool, program, addr(0x1006f56)); - - selectCheckBox("Case Sensitive", true); - - setAlignment("8"); - - setValueText("notepad"); - - List
addrs = addrs(0x010013e0); - - performSearchTest(addrs, "Previous"); - } - - @Test - public void testSearchBackwardAlign4NoneFound() throws Exception { - - goTo(tool, program, addr(0x1006f56)); - - selectCheckBox("Case Sensitive", true); - - setAlignment("8"); - - setValueText("Info"); - - pressSearchButton("Previous"); - assertEquals("Not Found", statusLabel.getText()); - } - - @Test - public void testSearchEscapeSequences() throws Exception { - selectCheckBox("Case Sensitive", true); - selectCheckBox("Escape Sequences", true); - - setEncoding(StandardCharsets.US_ASCII); - setValueText("\"Hello world!\"\\n\\t-new programmer"); - - List
addrs = addrs(0x0100189d, 0x0100198e); - - pressSearchButton("Next"); - assertEquals(addrs.get(0), cb.getCurrentLocation().getAddress()); - assertEquals("Found", statusLabel.getText()); - - pressSearchButton("Next"); - assertEquals("Not Found", statusLabel.getText()); - - setEncoding(StandardCharsets.UTF_16LE); - pressSearchButton("Next"); - assertEquals("Found", statusLabel.getText()); - - pressSearchButton("Next"); - assertEquals("Not Found", statusLabel.getText()); - } - -//================================================================================================== -// Private Methods -//================================================================================================== - - @SuppressWarnings("unchecked") - private void setEncoding(Charset encoding) throws Exception { - JComboBox encodingOptions = - (JComboBox) findComponentByName(pane, "Encoding Options", false); - - // Makes encoding UTF_16 in case encoding is UTF_16BE or UTF_16LE - // BE and LE are not choices in the combo box. - if (encoding == StandardCharsets.UTF_16BE || encoding == StandardCharsets.UTF_16LE) { - encoding = StandardCharsets.UTF_16; - } - - for (int i = 0; i < encodingOptions.getItemCount(); i++) { - if (encodingOptions.getItemAt(i) == encoding) { - int index = i; - runSwing(() -> encodingOptions.setSelectedIndex(index)); - break; - } - } - } -} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchBinaryTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchBinaryTest.java deleted file mode 100644 index 34bfd496cb..0000000000 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchBinaryTest.java +++ /dev/null @@ -1,559 +0,0 @@ -/* ### - * 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.plugin.core.searchmem; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.nio.charset.StandardCharsets; -import java.util.List; - -import javax.swing.DefaultComboBoxModel; - -import org.junit.Before; -import org.junit.Test; - -import docking.widgets.fieldpanel.support.Highlight; -import ghidra.app.events.ProgramLocationPluginEvent; -import ghidra.program.database.ProgramBuilder; -import ghidra.program.model.address.Address; -import ghidra.program.model.data.Pointer32DataType; -import ghidra.program.model.listing.*; -import ghidra.program.util.ProgramLocation; - -/** - * Tests for the Binary format in searching memory. - */ -public class MemSearchBinaryTest extends AbstractMemSearchTest { - - public MemSearchBinaryTest() { - super(); - } - - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - selectRadioButton("Binary"); - } - - @Override - protected Program buildProgram() throws Exception { - ProgramBuilder builder = new ProgramBuilder("TestX86", ProgramBuilder._X86); - builder.createMemory(".text", Long.toHexString(0x1001000), 0x6600); - builder.createMemory(".data", Long.toHexString(0x1008000), 0x600); - builder.createMemory(".rsrc", Long.toHexString(0x100A000), 0x5400); - builder.createMemory(".bound_import_table", Long.toHexString(0xF0000248), 0xA8); - builder.createMemory(".debug_data", Long.toHexString(0xF0001300), 0x1C); - - //create and disassemble a function - builder.setBytes( - "0x01002cf5", - "55 8b ec 83 7d 14 00 56 8b 35 e0 10 00 01 57 74 09 ff 75 14 ff d6 8b f8 eb 02 33 " + - "ff ff 75 10 ff d6 03 c7 8d 44 00 02 50 6a 40 ff 15 dc 10 00 01 8b f0 85 f6 74 27 " + - "56 ff 75 14 ff 75 10 e8 5c ff ff ff ff 75 18 ff 75 0c 56 ff 75 08 ff 15 04 12 00 " + - "01 56 8b f8 ff 15 c0 10 00 01 eb 14 ff 75 18 ff 75 0c ff 75 10 ff 75 08 ff 15 04 " + - "12 00 01 8b f8 8b c7 5f 5e 5d c2 14"); - builder.disassemble("0x01002cf5", 0x121, true); - builder.createFunction("0x01002cf5"); - - //create some data - - builder.setBytes("0x1001004", "85 4f dc 77"); - builder.applyDataType("0x1001004", new Pointer32DataType(), 1); - builder.createEncodedString("0x01001708", "Notepad", StandardCharsets.UTF_16BE, true); - builder.createEncodedString("0x01001740", "something else", StandardCharsets.UTF_16BE, true); - builder.createEncodedString("0x010013cc", "notepad.exe", StandardCharsets.US_ASCII, true); - - //create some undefined data - builder.setBytes("0x1001500", "4e 00 65 00 77 00"); - builder.setBytes("0x1003000", "55 00"); - - return builder.getProgram(); - } - - @Test - public void testBinaryInvalidEntry() { - // enter a non-binary digit; the search field should not accept it - setValueText("2"); - - assertEquals("", valueField.getText()); - } - - @Test - public void testBinaryMoreThan8Chars() throws Exception { - // try entering more than 8 binary digits (no spaces); the dialog - // should not accept the 9th digit. - myTypeText("010101010"); - assertEquals("01010101", valueField.getText()); - } - - @Test - public void testBinaryEnterSpaces() { - // verify that more than 8 digits are allowed if spaces are entered - myTypeText("01110000 01110000"); - assertEquals("01110000 01110000", valueField.getText()); - } - - @Test - public void testBinaryPasteNumberWithPrefix() { - // paste a number with a binary prefix; - // the prefix should be removed before the insertion - setValueText("0b00101010"); - assertEquals("00101010", valueField.getText()); - - setValueText("0B1010 10"); - assertEquals("1010 10", valueField.getText()); - } - - @Test - public void testBinarySearch() throws Exception { - - goTo(0x01001000); - - setValueText("00010100 11111111"); - - pressButtonByText(pane, "Next"); - - waitForSearchTask(); - - Address currentAddress = currentAddress(); - CodeUnit cu = codeUnitContaining(addr(0x01002d08)); - assertEquals(cu.getMinAddress(), currentAddress); - assertEquals("Found", statusLabel.getText()); - } - - @Test - public void testBinarySearchNext() throws Exception { - - goTo(0x01001000); - - setValueText("01110101"); - - //@formatter:off - List
addrs = addrs(0x01002d06, - 0x01002d11, - 0x01002d2c, - 0x01002d2f, - 0x01002d37, - 0x01002d3a, - 0x01002d3e, - 0x01002d52, - 0x01002d55, - 0x01002d58, - 0x01002d5b); - //@formatter:on - - for (int i = 0; i < addrs.size(); i++) { - Address start = addrs.get(i); - pressSearchButton("Next"); - CodeUnit cu = listing.getCodeUnitContaining(start); - assertEquals(cu.getMinAddress(), cb.getCurrentLocation().getAddress()); - assertEquals("Found", statusLabel.getText()); - } - pressSearchButton("Next"); - assertEquals("Not Found", statusLabel.getText()); - - } - - @Test - public void testBinarySearchNextAlign4() throws Exception { - // hit the enter key in the values field; - // should go to next match found - - Address addr = addr(0x01001000); - tool.firePluginEvent(new ProgramLocationPluginEvent("test", new ProgramLocation(program, - addr), program)); - waitForSwing(); - - // enter a Binary value and hit the search button - setValueText("01110101"); - - setAlignment("4"); - - //the bytes are at the right alignment value but the code units are not - List
addrs = addrs(0x01002d2f, 0x01002d37, 0x01002d5b); - - for (int i = 0; i < addrs.size(); i++) { - Address start = addrs.get(i); - pressSearchButton("Next"); - CodeUnit cu = listing.getCodeUnitContaining(start); - assertEquals(cu.getMinAddress(), cb.getCurrentLocation().getAddress()); - assertEquals("Found", statusLabel.getText()); - } - pressSearchButton("Next"); - assertEquals("Not Found", statusLabel.getText()); - } - - @Test - public void testBinaryContiguousSelection() throws Exception { - - goTo(0x01001070); - - makeSelection(tool, program, range(0x01002cf5, 0x01002d6d)); - - assertSearchSelectionSelected(); - - setValueText("11110110"); - - // the bytes are at the right alignment value but the code units are not - performSearchTest(addrs(0x01002d27), "Next"); - } - - @Test - public void testBinaryNonContiguousSelection() throws Exception { - - makeSelection(tool, program, range(0x01002cf5, 0x01002d0e), range(0x01002d47, 0x01002d51)); - - assertSearchSelectionSelected(); - - setValueText("01010110"); - - // the bytes are at the right alignment value but the code units are not - List
addrs = addrs(0x01002cfc, 0x01002d47); - - performSearchTest(addrs, "Next"); - } - - @Test - public void testBinarySelectionNotOn() throws Exception { - - goTo(0x01002cf5); - - // make a selection but turn off the Selection checkbox; - // the search should go outside the selection - - makeSelection(tool, program, range(0x01002cf5, 0x01002d0d), range(0x01002d37, 0x01002d47)); - - // select Search All option to turn off searching only in selection - assertButtonState("Search All", true, false); - - // Note: this is 'Search All' for the search type, not the JButton on the button panel - pressButtonByText(pane, "Search All"); - - setValueText("11110110"); - - pressButtonByText(pane, "Next"); - waitForSearchTask(); - - Address resultAddr = addr(0x1002d27); - - // verify the code browser goes to resulting address - CodeUnit cu = codeUnitContaining(resultAddr); - assertEquals(cu.getMinAddress(), currentAddress()); - assertEquals("Found", statusLabel.getText()); - } - - @Test - public void testBinarySearchAll() throws Exception { - // QueryResults should get displayed - // test the marker stuff - setValueText("11110110"); - - pressSearchAllButton(); - - waitForSearch("Search Memory - ", 1); - - checkMarkerSet(addrs(0x1002d28)); - } - - @Test - public void testBinarySearchAll2() throws Exception { - // enter search string for multiple byte match - // ff d6 - setValueText("11111111 11010110"); - - pressSearchAllButton(); - - waitForSearch("Search Memory - ", 2); - - List
addrs = addrs(0x1002d09, 0x1002d14); - - checkMarkerSet(addrs); - } - - @Test - public void testBinarySearchAllAlign4() throws Exception { - // QueryResults should get displayed - // test the marker stuff - setValueText("11111111 01110101"); - - setAlignment("4"); - - pressSearchAllButton(); - waitForSearch("Search Memory - ", 2); - - List
startList = addrs(0x1002d2c, 0x1002d58); - - checkMarkerSet(startList); - } - - @Test - public void testBinaryHighlight() throws Exception { - - setValueText("00010000 00000000 00000001"); - - pressSearchAllButton(); - - waitForSearch("Search Memory - ", 3); - - Highlight[] h = getByteHighlights(addr(0x1002cfd), "8b 35 e0 10 00 01"); - assertEquals(1, h.length); - assertEquals(9, h[0].getStart()); - assertEquals(16, h[0].getEnd()); - } - - @Test - public void testBinarySearchSelection() throws Exception { - - goTo(0x01001074); - - makeSelection(tool, program, range(0x01002cf5, 0x01002d6d)); - - assertSearchSelectionSelected(); - - setValueText("11110110"); - - performSearchTest(addrs(0x01002d27), "Next"); - } - - @Test - public void testBinarySearchPreviousNotFound() throws Exception { - - goTo(0x01001000); - - setValueText("00000111"); - pressButtonByText(pane, "Previous"); - waitForSearchTask(); - - assertEquals("Not Found", statusLabel.getText()); - } - - @Test - public void testCodeUnitScope_Instructions() throws Exception { - // - // Turn on Instructions scope and make sure only that scope yields matches - // - goTo(0x1002cf5); - - selectCheckBox("Instructions", true); - selectCheckBox("Defined Data", false); - selectCheckBox("Undefined Data", false); - - setValueText("01010101"); - pressSearchButton("Next"); - - Address expectedSearchAddressHit = addr(0x1002cf5); - assertEquals( - "Did not find a hit at the next matching Instruction when we are searching Instructions", - expectedSearchAddressHit, currentAddress()); - - // Turn off Instructions scope and make sure we have no match at the expected address - goTo(0x1002cf5); - - selectCheckBox("Instructions", false); - selectCheckBox("Defined Data", true); - selectCheckBox("Undefined Data", true); - pressSearchButton("Next"); - - assertTrue( - "Found a search match at an Instruction, even though no Instruction should be searched", - !expectedSearchAddressHit.equals(currentAddress())); - - CodeUnit codeUnit = currentCodeUnit(); - assertTrue("Did not find a data match when searching instructions is disabled", - codeUnit instanceof Data); - } - - @Test - public void testCodeUnitScope_DefinedData() throws Exception { - // - // Turn on Defined Data scope and make sure only that scope yields matches - // - goTo(0x1001000);// start of program; pointer data - - selectCheckBox("Instructions", false); - selectCheckBox("Defined Data", true); - selectCheckBox("Undefined Data", false); - - setValueText("10000101"); - pressSearchButton("Next"); - Address expectedSearchAddressHit = addr(0x1001004); - - assertEquals( - "Did not find a hit at the next matching Defined Data when we are searching Defined Data", - expectedSearchAddressHit, currentAddress()); - - // Turn off Defined Data scope and make sure we have no match at the expected address - goTo(0x1001000);// start of program; pointer data - - selectCheckBox("Instructions", true); - selectCheckBox("Defined Data", false); - selectCheckBox("Undefined Data", true); - pressSearchButton("Next"); - assertTrue( - "Found a search match at a Defined Data, even though no Defined Data should be searched", - !expectedSearchAddressHit.equals(currentAddress())); - - CodeUnit codeUnit = currentCodeUnit(); - assertTrue("Did not find a instruction match when searching defined data is disabled", - codeUnit instanceof Instruction); - - // try backwards - goTo(0x1002000); - assertEquals( - "Did not find a hit at the next matching Defined Data when we are searching Defined Data", - addr(0x1002000), currentAddress()); - - selectCheckBox("Instructions", false); - selectCheckBox("Defined Data", true); - selectCheckBox("Undefined Data", false); - - pressSearchButton("Previous"); - expectedSearchAddressHit = addr(0x01001004); - assertEquals( - "Did not find a hit at the previous matching Defined Data when we are searching Defined Data", - expectedSearchAddressHit, currentAddress()); - } - - @Test - public void testCodeUnitScope_UndefinedData() throws Exception { - // - // Turn on Undefined Data scope and make sure only that scope yields matches - // - goTo(0x1001000); - - selectCheckBox("Instructions", false); - selectCheckBox("Defined Data", false); - selectCheckBox("Undefined Data", true); - - setValueText("01100101"); - pressSearchButton("Next"); - - Address expectedSearchAddressHit = addr(0x1001502); - assertEquals( - "Did not find a hit at the next matching Undefined Data when we are searching Undefined Data", - expectedSearchAddressHit, currentAddress()); - - // Turn off Undefined Data scope and make sure we have no match at the expected address - goTo(0x1001000); - - selectCheckBox("Instructions", true); - selectCheckBox("Defined Data", true); - selectCheckBox("Undefined Data", false); - pressSearchButton("Next"); - assertTrue( - "Found a search match at an Undefined Data, even though no Undefined Data should be searched", - !expectedSearchAddressHit.equals(currentAddress())); - - CodeUnit codeUnit = listing.getCodeUnitAt(cb.getCurrentLocation().getAddress()); - assertTrue("Did not find a instruction match when searching defined data is disabled", - codeUnit instanceof Data); - - // try backwards - - goTo(0x1003000); - - selectCheckBox("Instructions", false); - selectCheckBox("Defined Data", false); - selectCheckBox("Undefined Data", true); - - pressSearchButton("Previous"); - expectedSearchAddressHit = addr(0x1001502); - assertEquals( - "Did not find a hit at the previous matching Undefined Data when we are searching Undefined Data", - expectedSearchAddressHit, currentAddress()); - } - - @Test - public void testBinarySearchPrevious() throws Exception { - // enter search string for multiple byte match - // ff 15 - - // start at 01002d6b - goTo(0x01002d6b); - - setValueText("11111111 00010101"); - - List
addrs = addrs(0x01002d5e, 0x01002d4a, 0x01002d41, 0x01002d1f); - - performSearchTest(addrs, "Previous"); - } - - @Test - public void testBinarySearchPreviousAlign4() throws Exception { - // enter search string for multiple byte match - // ff 15 - - goTo(0x1002d6d); - - setValueText("11111111 01110101"); - - setAlignment("4"); - - List
addrs = addrs(0x1002d58, 0x1002d2c); - - performSearchTest(addrs, "Previous"); - } - - @Test - public void testBinaryWildcardSearch() throws Exception { - goTo(0x01001000); - - setValueText("010101xx 10001011"); - - List
addrs = addrs(0x01002cf5, 0x01002cfc, 0x01002d47); - - performSearchTest(addrs, "Next"); - } - - @Test - public void testBinaryWildcardSearchAll() throws Exception { - - setValueText("10001011 1111xxxx"); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 4); - - List
addrs = addrs(0x1002d0b, 0x1002d25, 0x1002d48, 0x1002d64); - - checkMarkerSet(addrs); - } - - @SuppressWarnings("rawtypes") - @Test - public void testValueComboBox() throws Exception { - setValueText("1x1xx1x1"); - - pressSearchButton("Next"); - setValueText(""); - - setValueText("00000"); - pressSearchButton("Next"); - setValueText(""); - - setValueText("111"); - pressSearchButton("Next"); - setValueText(""); - - // the combo box should list most recently entered values - DefaultComboBoxModel cbModel = (DefaultComboBoxModel) valueComboBox.getModel(); - assertEquals(3, cbModel.getSize()); - assertEquals("111", cbModel.getElementAt(0)); - assertEquals("00000", cbModel.getElementAt(1)); - assertEquals("1x1xx1x1", cbModel.getElementAt(2)); - } - -} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchDecimal2Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchDecimal2Test.java deleted file mode 100644 index 2ca2d9e490..0000000000 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchDecimal2Test.java +++ /dev/null @@ -1,668 +0,0 @@ -/* ### - * 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.plugin.core.searchmem; - -import static org.junit.Assert.*; - -import java.awt.Container; -import java.nio.charset.StandardCharsets; -import java.util.List; - -import javax.swing.*; -import javax.swing.border.Border; -import javax.swing.border.TitledBorder; - -import org.junit.Before; -import org.junit.Test; - -import ghidra.program.database.ProgramBuilder; -import ghidra.program.model.address.Address; -import ghidra.program.model.data.Pointer32DataType; -import ghidra.program.model.listing.Program; - -/** - * Tests for searching for decimal values in memory. - */ -public class MemSearchDecimal2Test extends AbstractMemSearchTest { - - public MemSearchDecimal2Test() { - super(); - } - - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - selectRadioButton("Decimal"); - } - - @Override - protected Program buildProgram() throws Exception { - ProgramBuilder builder = new ProgramBuilder("TestX86", ProgramBuilder._X86); - builder.createMemory(".text", Long.toHexString(0x1001000), 0x6600); - builder.createMemory(".data", Long.toHexString(0x1008000), 0x600); - builder.createMemory(".rsrc", Long.toHexString(0x100A000), 0x5400); - builder.createMemory(".bound_import_table", Long.toHexString(0xF0000248), 0xA8); - builder.createMemory(".debug_data", Long.toHexString(0xF0001300), 0x1C); - - //create and disassemble a function - builder.setBytes( - "0x01002cf5", - "55 8b ec 83 7d 14 00 56 8b 35 e0 10 00 01 57 74 09 ff 75 14 ff d6 8b f8 eb 02 " + - "33 ff ff 75 10 ff d6 03 c7 8d 44 00 02 50 6a 40 ff 15 dc 10 00 01 8b f0 85 f6 " + - "74 27 56 ff 75 14 ff 75 10 e8 5c ff ff ff ff 75 18 ff 75 0c 56 ff 75 08 ff 15 " + - "04 12 00 01 56 8b f8 ff 15 c0 10 00 01 eb 14 ff 75 18 ff 75 0c ff 75 10 ff 75 " + - "08 ff 15 04 12 00 01 8b f8 8b c7 5f 5e 5d c2 14"); - builder.disassemble("0x01002cf5", 0x121, true); - builder.createFunction("0x01002cf5"); - - //create some data - - builder.setBytes("0x1001004", "85 4f dc 77"); - builder.applyDataType("0x1001004", new Pointer32DataType(), 1); - builder.createEncodedString("0x01001708", "Notepad", StandardCharsets.UTF_16BE, true); - builder.createEncodedString("0x01001740", "something else", StandardCharsets.UTF_16BE, true); - builder.createEncodedString("0x010013cc", "notepad.exe", StandardCharsets.US_ASCII, false); - - //create some undefined data - builder.setBytes("0x1001500", "4e 00 65 00 77 00"); - builder.setBytes("0x1003000", "55 00"); - builder.setBytes("0x1004100", "64 00 00 00");//100 dec - builder.setBytes("0x1004120", "50 ff 75 08");//7.4027124e-34 float - builder.setBytes("0x1004135", "64 00 00 00");//100 dec - builder.setBytes("0x1004200", "50 ff 75 08 e8 8d 3c 00");//1.588386874245921e-307 - builder.setBytes("0x1004247", "50 ff 75 08");//7.4027124e-34 float - builder.setBytes("0x1004270", "65 00 6e 00 64 00 69 00");//29555302058557541 qword - - return builder.getProgram(); - } - - @Test - public void testDecimalOptionsShowing() throws Exception { - // select the Decimal option; verify radio buttons for decimal types - // are showing in the Decimal Options panel. - JRadioButton rb = (JRadioButton) findAbstractButtonByText(pane, "Byte"); - assertNotNull(rb); - JPanel p = findTitledJPanel(rb, "Format Options"); - assertNotNull(p); - assertTrue(p.isVisible()); - - assertTrue(!rb.isSelected()); - assertTrue(rb.isVisible()); - - rb = (JRadioButton) findAbstractButtonByText(pane, "Word"); - assertNotNull(rb); - assertTrue(rb.isSelected()); - assertTrue(rb.isVisible()); - - rb = (JRadioButton) findAbstractButtonByText(pane, "DWord"); - assertNotNull(rb); - assertTrue(!rb.isSelected()); - assertTrue(rb.isVisible()); - - rb = (JRadioButton) findAbstractButtonByText(pane, "QWord"); - assertNotNull(rb); - assertTrue(!rb.isSelected()); - assertTrue(rb.isVisible()); - - rb = (JRadioButton) findAbstractButtonByText(pane, "Float"); - assertNotNull(rb); - assertTrue(!rb.isSelected()); - assertTrue(rb.isVisible()); - - rb = (JRadioButton) findAbstractButtonByText(pane, "Double"); - assertNotNull(rb); - assertTrue(!rb.isSelected()); - assertTrue(rb.isVisible()); - } - - @Test - public void testInvalidEntry() throws Exception { - // enter non-numeric value - setValueText("z"); - assertEquals("", valueField.getText()); - assertEquals("", hexLabel.getText()); - } - - @Test - public void testValueTooLarge() throws Exception { - // select "Byte" and enter 260; should not accept "0" - selectRadioButton("Byte"); - - myTypeText("260"); - assertEquals("26", valueField.getText()); - assertEquals(statusLabel.getText(), "Number must be in the range [-128,255]"); - } - - @Test - public void testValueTooLarge2() throws Exception { - // select "Word" and enter 2698990; should not accept "26989" - selectRadioButton("Word"); - - myTypeText("2698990"); - assertEquals(statusLabel.getText(), "Number must be in the range [-32768,65535]"); - assertEquals("26989", valueField.getText()); - } - - @Test - public void testNegativeValueEntered() throws Exception { - // enter a negative value; the hexLabel should show the correct - // byte sequence - - setValueText("-1234"); - assertEquals("2e fb ", hexLabel.getText()); - - selectRadioButton("Byte"); - assertEquals("", valueField.getText()); - assertEquals("", hexLabel.getText()); - setValueText("-55"); - assertEquals("c9 ", hexLabel.getText()); - - selectRadioButton("DWord"); - assertEquals("c9 ff ff ff ", hexLabel.getText()); - - selectRadioButton("QWord"); - assertEquals("c9 ff ff ff ff ff ff ff ", hexLabel.getText()); - - selectRadioButton("Float"); - assertEquals("00 00 5c c2 ", hexLabel.getText()); - - selectRadioButton("Double"); - assertEquals("00 00 00 00 00 80 4b c0 ", hexLabel.getText()); - } - - @Test - public void testMulipleValuesEntered() throws Exception { - // enter values separated by a space; values should be accepted - selectRadioButton("Byte"); - setValueText("12 34 56 78"); - assertEquals("0c 22 38 4e ", hexLabel.getText()); - - selectRadioButton("Word"); - assertEquals("0c 00 22 00 38 00 4e 00 ", hexLabel.getText()); - - selectRadioButton("DWord"); - assertEquals("0c 00 00 00 22 00 00 00 38 00 00 00 4e 00 00 00 ", hexLabel.getText()); - - selectRadioButton("QWord"); - assertEquals("0c 00 00 00 00 00 00 00 22 00 00 00 00 00 00 00 " - + "38 00 00 00 00 00 00 00 4e 00 00 00 00 00 00 00 ", hexLabel.getText()); - - selectRadioButton("Float"); - assertEquals("00 00 40 41 00 00 08 42 00 00 60 42 00 00 9c 42 ", hexLabel.getText()); - - selectRadioButton("Double"); - assertEquals("00 00 00 00 00 00 28 40 00 00 00 00 00 00 41 40 " - + "00 00 00 00 00 00 4c 40 00 00 00 00 00 80 53 40 ", hexLabel.getText()); - } - - @Test - public void testByteOrder() throws Exception { - setValueText("12 34 56 78"); - selectRadioButton("Byte"); - selectRadioButton("Big Endian"); - // should be unaffected - assertEquals("0c 22 38 4e ", hexLabel.getText()); - - selectRadioButton("Word"); - assertEquals("00 0c 00 22 00 38 00 4e ", hexLabel.getText()); - - selectRadioButton("DWord"); - assertEquals("00 00 00 0c 00 00 00 22 00 00 00 38 00 00 00 4e ", hexLabel.getText()); - - selectRadioButton("QWord"); - assertEquals("00 00 00 00 00 00 00 0c 00 00 00 00 00 00 00 22 " - + "00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 4e ", hexLabel.getText()); - - selectRadioButton("Float"); - assertEquals("41 40 00 00 42 08 00 00 42 60 00 00 42 9c 00 00 ", hexLabel.getText()); - - selectRadioButton("Double"); - assertEquals("40 28 00 00 00 00 00 00 40 41 00 00 00 00 00 00 " - + "40 4c 00 00 00 00 00 00 40 53 80 00 00 00 00 00 ", hexLabel.getText()); - } - - @Test - public void testFloatDoubleFormat() throws Exception { - selectRadioButton("Float"); - - setValueText("12.345"); - assertEquals("12.345", valueField.getText()); - assertEquals("1f 85 45 41 ", hexLabel.getText()); - - selectRadioButton("Double"); - assertEquals("71 3d 0a d7 a3 b0 28 40 ", hexLabel.getText()); - } - - @Test - public void testSearchByte() throws Exception { - goTo(program.getMinAddress()); - - List
addrs = addrs(0x1002d3e, 0x1002d5b, 0x1004123, 0x1004203, 0x100424a); - - selectRadioButton("Byte"); - setValueText("8"); - - performSearchTest(addrs, "Next"); - } - - @Test - public void testSearchWord() throws Exception { - - goTo(program.getMinAddress()); - - selectRadioButton("Word"); - - setValueText("20"); - - List
addrs = addrs(0x1002cf8, 0x1002d6b); - - performSearchTest(addrs, "Next"); - } - - @Test - public void testSearchWordBackward() throws Exception { - - goTo(0x01002d6e); - - selectRadioButton("Word"); - - setValueText("20"); - - List
addrs = addrs(0x1002d6b, 0x1002cf8); - - performSearchTest(addrs, "Previous"); - } - - @Test - public void testSearchDWord() throws Exception { - goTo(program.getMinAddress()); - - selectRadioButton("DWord"); - - setValueText("100"); - - List
addrs = addrs(0x1001708, 0x1004100, 0x1004135); - - performSearchTest(addrs, "Next"); - } - - @Test - public void testSearchDWordBackward() throws Exception { - goTo(0x01005000); - - selectRadioButton("DWord"); - - setValueText("100"); - - List
addrs = addrs(0x1004135, 0x1004100, 0x1001708); - - performSearchTest(addrs, "Previous"); - } - - @Test - public void testSearchQWord() throws Exception { - goTo(program.getMinAddress()); - - selectRadioButton("QWord"); - - setValueText("29555302058557541"); - - performSearchTest(addrs(0x1004270), "Next"); - } - - @Test - public void testSearchQWordBackward() throws Exception { - - goTo(program.getMaxAddress()); - - selectRadioButton("QWord"); - - setValueText("29555302058557541"); - - performSearchTest(addrs(0x1004270), "Previous"); - } - - @Test - public void testSearchFloat() throws Exception { - - goTo(program.getMinAddress()); - - selectRadioButton("Float"); - - setValueText("7.4027124e-34"); - - List
addrs = addrs(0x1004120, 0x1004200, 0x1004247); - - performSearchTest(addrs, "Next"); - } - - @Test - public void testSearchFloatBackward() throws Exception { - - goTo(0x01005000); - - selectRadioButton("Float"); - - setValueText("7.4027124e-34"); - - List
addrs = addrs(0x1004247, 0x1004200, 0x1004120); - - performSearchTest(addrs, "Previous"); - } - - @Test - public void testSearchFloatBackwardAlign8() throws Exception { - - goTo(program.getMaxAddress()); - - JTextField alignment = (JTextField) findComponentByName(dialog.getComponent(), "Alignment"); - setText(alignment, "8"); - - selectRadioButton("Float"); - - setValueText("7.4027124e-34"); - - List
addrs = addrs(0x1004200, 0x1004120); - - performSearchTest(addrs, "Previous"); - } - - @Test - public void testSearchDouble() throws Exception { - - goTo(program.getMinAddress()); - - selectRadioButton("Double"); - - setValueText("1.588386874245921e-307"); - - List
addrs = addrs(0x1004200); - - performSearchTest(addrs, "Next"); - } - - @Test - public void testSearchDoubleBackward() throws Exception { - - goTo(program.getMaxAddress()); - - selectRadioButton("Double"); - - setValueText("1.588386874245921e-307"); - - List
addrs = addrs(0x1004200); - - performSearchTest(addrs, "Previous"); - } - - @Test - public void testSearchAllByte() throws Exception { - - selectRadioButton("Byte"); - - setValueText("8"); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 5); - - List
addrs = addrs(0x1002d40, 0x1002d5d, 0x1004123, 0x1004203, 0x100424a); - - checkMarkerSet(addrs); - } - - @Test - public void testSearchAllWord() throws Exception { - - selectRadioButton("Word"); - - setValueText("20"); - - pressSearchAllButton(); - waitForSearch("Search Memory - ", 2); - - List
addrs = addrs(0x1002cfa, 0x1002d6c); - - checkMarkerSet(addrs); - } - - @Test - public void testSearchAllWordAlign4() throws Exception { - - JTextField alignment = (JTextField) findComponentByName(dialog.getComponent(), "Alignment"); - setText(alignment, "4"); - - selectRadioButton("Word"); - - setValueText("20"); - - pressSearchAllButton(); - waitForSearch("Search Memory - ", 1); - - checkMarkerSet(addrs(0x1002d6c)); - } - - @Test - public void testSearchAllDWord() throws Exception { - - selectRadioButton("DWord"); - - setValueText("100"); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 3); - - List
addrs = addrs(0x1001715, 0x1004100, 0x1004135); - - checkMarkerSet(addrs); - } - - @Test - public void testSearchAllQWord() throws Exception { - - selectRadioButton("QWord"); - - setValueText("29555302058557541"); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 1); - - checkMarkerSet(addrs(0x1004270)); - } - - @Test - public void testSearchAllFloat() throws Exception { - - selectRadioButton("Float"); - - setValueText("7.4027124e-34"); - - pressSearchAllButton(); - waitForSearch("Search Memory - ", 3); - - List
addrs = addrs(0x1004120, 0x1004200, 0x1004247); - - checkMarkerSet(addrs); - } - - @Test - public void testSearchAllDouble() throws Exception { - - selectRadioButton("Double"); - - setValueText("1.588386874245921e-307"); - - pressSearchAllButton(); - waitForSearch("Search Memory - ", 1); - - checkMarkerSet(addrs(0x1004200)); - } - - @Test - public void testSearchSelectionByte() throws Exception { - - makeSelection(tool, program, range(0x01004000, 0x01005000)); - - assertSearchSelectionSelected(); - - selectRadioButton("Byte"); - - setValueText("8"); - - List
addrs = addrs(0x1004123, 0x1004203, 0x100424a); - - performSearchTest(addrs, "Next"); - } - - @Test - public void testSearchSelectionWord() throws Exception { - - makeSelection(tool, program, range(0x01002c00, 0x01002d00)); - - assertSearchSelectionSelected(); - - selectRadioButton("Word"); - - setValueText("20"); - - performSearchTest(addrs(0x1002cf8), "Next"); - } - - @Test - public void testSearchSelectionDWord() throws Exception { - - makeSelection(tool, program, range(0x01004000, 0x01005000)); - - assertSearchSelectionSelected(); - - selectRadioButton("DWord"); - - setValueText("100"); - - List
addrs = addrs(0x1004100, 0x1004135); - - performSearchTest(addrs, "Next"); - } - - @Test - public void testSearchSelectionQWord() throws Exception { - - makeSelection(tool, program, range(0x01004000, 0x01005000)); - - assertSearchSelectionSelected(); - - selectRadioButton("QWord"); - - setValueText("29555302058557541"); - - performSearchTest(addrs(0x1004270), "Next"); - - } - - @Test - public void testSearchSelectionFloat() throws Exception { - - makeSelection(tool, program, range(0x01004200, 0x01004300)); - - assertSearchSelectionSelected(); - - selectRadioButton("Float"); - - setValueText("7.4027124e-34"); - - List
addrs = addrs(0x1004200, 0x1004247); - - performSearchTest(addrs, "Next"); - } - - @Test - public void testSearchSelectionDouble() throws Exception { - - makeSelection(tool, program, range(0x01004000, 0x01005000)); - - assertSearchSelectionSelected(); - - selectRadioButton("Double"); - - setValueText("1.588386874245921e-307"); - - performSearchTest(addrs(0x1004200), "Next"); - - } - - @Test - public void testSearchAllInSelection() throws Exception { - - makeSelection(tool, program, range(0x01002cf5, 0x01002d6d)); - - assertSearchSelectionSelected(); - - selectRadioButton("Byte"); - - setValueText("8"); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 2); - - List
addrs = addrs(0x1002d40, 0x1002d5d); - - checkMarkerSet(addrs); - } - - @Test - public void testSearchBackwardsInSelection() throws Exception { - - goTo(program.getMaxAddress()); - - makeSelection(tool, program, range(0x01004000, 0x01005000)); - - assertSearchSelectionSelected(); - - selectRadioButton("Double"); - - setValueText("1.588386874245921e-307"); - - performSearchTest(addrs(0x1004200), "Previous"); - } - -//================================================================================================== -// Private Methods -//================================================================================================== - - @Override - protected void showMemSearchDialog() { - super.showMemSearchDialog(); - selectRadioButton("Decimal"); - } - - private JPanel findTitledJPanel(Container container, String title) { - if (container instanceof JPanel) { - JPanel p = (JPanel) container; - Border b = p.getBorder(); - if ((b instanceof TitledBorder) && ((TitledBorder) b).getTitle().equals(title)) { - return p; - } - } - Container parent = container.getParent(); - while (parent != null) { - if (parent instanceof JPanel) { - JPanel p = findTitledJPanel(parent, title); - if (p != null) { - return p; - } - } - parent = parent.getParent(); - } - return null; - } - -} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/AbstractMemSearchTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/AbstractMemSearchTest.java new file mode 100644 index 0000000000..2237c273bc --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/AbstractMemSearchTest.java @@ -0,0 +1,289 @@ +/* ### + * 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.features.base.memsearch; + +import static org.junit.Assert.*; + +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.collections4.IteratorUtils; + +import docking.action.DockingActionIf; +import docking.widgets.fieldpanel.support.Highlight; +import ghidra.app.plugin.core.codebrowser.CodeViewerProvider; +import ghidra.app.services.*; +import ghidra.app.util.ListingHighlightProvider; +import ghidra.app.util.viewer.field.BytesFieldFactory; +import ghidra.app.util.viewer.field.ListingField; +import ghidra.app.util.viewer.format.FormatManager; +import ghidra.features.base.memsearch.bytesource.SearchRegion; +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.features.base.memsearch.gui.*; +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.Memory; +import ghidra.test.AbstractProgramBasedTest; +import ghidra.util.Swing; + +/** + * Base class for memory search tests. + */ +public abstract class AbstractMemSearchTest extends AbstractProgramBasedTest { + + protected MemorySearchPlugin memorySearchPlugin; + protected DockingActionIf searchAction; + protected CodeViewerProvider provider; + protected Memory memory; + protected Listing listing; + protected MarkerService markerService; + + protected MemorySearchProvider searchProvider; + private SearchSettings settings = new SearchSettings(); + + /* + * Note that this setup function does not have the @Before annotation - this is because + * sub-classes often need to override this and if we have the annotation here, the test + * runner will only invoke this base class implementation. + */ + public void setUp() throws Exception { + + // this builds the program and launches the tool + initialize(); + + memorySearchPlugin = env.getPlugin(MemorySearchPlugin.class); + + listing = program.getListing(); + memory = program.getMemory(); + + searchAction = getAction(memorySearchPlugin, "Memory Search"); + + provider = codeBrowser.getProvider(); + markerService = tool.getService(MarkerService.class); + + showMemorySearchProvider(); + } + + protected void setInput(String input) { + Swing.runNow(() -> searchProvider.setSearchInput(input)); + } + + @Override + protected Program getProgram() throws Exception { + return buildProgram(); + } + + protected abstract Program buildProgram() throws Exception; + + protected void waitForSearch(int expectedResults) { + + waitForCondition(() -> { + return runSwing(() -> !searchProvider.isBusy()); + }, "Timed-out waiting for search results"); + assertEquals(expectedResults, searchProvider.getSearchResults().size()); + } + + protected void waitForSearchTask() { + waitForSwing(); + waitForTasks(); + waitForSwing(); + } + + protected void showMemorySearchProvider() { + performAction(searchAction, provider, true); + searchProvider = waitForComponentProvider(MemorySearchProvider.class); + } + + @SuppressWarnings("unchecked") + private List
getHighlightAddresses() { + CodeViewerService service = tool.getService(CodeViewerService.class); + Object codeViewerProvider = getInstanceField("connectedProvider", service); + Map highlighterMap = + (Map) getInstanceField("programHighlighterMap", + codeViewerProvider); + ListingHighlightProvider highlightProvider = highlighterMap.get(program); + + assertEquals("The inner-class has been renamed", "MemoryMatchHighlighter", + highlightProvider.getClass().getSimpleName()); + + List data = searchProvider.getSearchResults(); + return data.stream().map(result -> result.getAddress()).collect(Collectors.toList()); + } + + protected void checkMarkerSet(List
expected) { + List
highlights = getHighlightAddresses(); + assertListEqualUnordered("Search highlights not correctly generated", expected, highlights); + + MarkerSet markers = + runSwing(() -> markerService.getMarkerSet(searchProvider.getTitle(), program)); + assertNotNull(markers); + + AddressSet addressSet = runSwing(() -> markers.getAddressSet()); + AddressIterator it = addressSet.getAddresses(true); + List
list = IteratorUtils.toList(it); + + assertListEqualUnordered("Search markers not correctly generated", expected, list); + } + + protected void performSearchNext(Address expected) throws Exception { + DockingActionIf action = getAction(tool, "MemorySearchPlugin", "Search Next"); + performAction(action); + waitForSearchTask(); + codeBrowser.updateNow(); + assertEquals(expected, codeBrowser.getCurrentAddress()); + } + + protected void performSearchNext(List
expected) throws Exception { + DockingActionIf action = getAction(tool, "MemorySearchPlugin", "Search Next"); + performSearchNextPrevious(expected, action); + } + + protected void performSearchPrevious(List
expected) throws Exception { + DockingActionIf action = getAction(tool, "MemorySearchPlugin", "Search Previous"); + performSearchNextPrevious(expected, action); + } + + protected void performSearchNextPrevious(List
expected, DockingActionIf action) + throws Exception { + + for (Address addr : expected) { + performAction(action); + waitForSearchTask(); + codeBrowser.updateNow(); + assertEquals(addr, codeBrowser.getCurrentAddress()); + } + + Address addr = codeBrowser.getCurrentAddress(); + performAction(action); + waitForSearchTask(); + codeBrowser.updateNow(); + assertEquals(addr, codeBrowser.getCurrentAddress()); + } + + protected void performSearchAll() { + runSwing(() -> searchProvider.search()); + } + + protected ListingHighlightProvider getHighlightProvider() { + CodeViewerService service = tool.getService(CodeViewerService.class); + FormatManager fm = (FormatManager) getInstanceField("formatMgr", service); + return (ListingHighlightProvider) getInstanceField("highlightProvider", fm); + } + + protected void repeatSearchForward() { + DockingActionIf action = getAction(memorySearchPlugin, "Repeat Memory Search Forwards"); + assertTrue(action.isEnabled()); + performAction(action, provider, true); + waitForSearchTask(); + } + + protected void repeatSearchBackward() { + DockingActionIf action = getAction(memorySearchPlugin, "Repeat Memory Search Backwards"); + assertTrue(action.isEnabled()); + performAction(action, provider, true); + waitForSearchTask(); + } + + protected Address currentAddress() { + codeBrowser.updateNow(); + Address addr = codeBrowser.getCurrentLocation().getAddress(); + return addr; + } + + protected CodeUnit currentCodeUnit() { + CodeUnit cu = program.getListing().getCodeUnitContaining(currentAddress()); + return cu; + } + + protected CodeUnit codeUnitContaining(Address addr) { + CodeUnit cu = program.getListing().getCodeUnitContaining(addr); + return cu; + } + + protected void assertSearchSelectionSelected() { + waitForSwing(); + assertTrue(Swing.runNow(() -> searchProvider.isSearchSelection())); + } + + protected Highlight[] getByteHighlights(Address address, String bytes) { + goTo(address); + ListingHighlightProvider hlProvider = getHighlightProvider(); + ListingField field = getField(address, BytesFieldFactory.FIELD_NAME); + return hlProvider.createHighlights(bytes, field, -1); + } + + protected String getInput() { + return Swing.runNow(() -> searchProvider.getSearchInput()); + } + + protected String getByteString() { + return Swing.runNow(() -> searchProvider.getByteString()); + } + + protected void setSearchFormat(SearchFormat format) { + settings = settings.withSearchFormat(format); + runSwing(() -> searchProvider.setSettings(settings)); + } + + protected void setDecimalSize(int size) { + settings = settings.withDecimalByteSize(size); + runSwing(() -> searchProvider.setSettings(settings)); + } + + protected void setAlignment(int alignment) { + settings = settings.withAlignment(alignment); + runSwing(() -> searchProvider.setSettings(settings)); + } + + protected void setSearchSelectionOnly(boolean b) { + runSwing(() -> searchProvider.setSearchSelectionOnly(b)); + } + + protected void setBigEndian(boolean b) { + settings = settings.withBigEndian(b); + runSwing(() -> searchProvider.setSettings(settings)); + } + + protected void setCaseSensitive(boolean b) { + settings = settings.withCaseSensitive(b); + runSwing(() -> searchProvider.setSettings(settings)); + } + + protected void setCharset(Charset charset) { + settings = settings.withStringCharset(charset); + runSwing(() -> searchProvider.setSettings(settings)); + } + + protected void setEscapeSequences(boolean b) { + settings = settings.withUseEscapeSequence(b); + runSwing(() -> searchProvider.setSettings(settings)); + } + + protected void addSearchRegion(SearchRegion region, boolean b) { + settings = settings.withSelectedRegion(region, b); + runSwing(() -> searchProvider.setSettings(settings)); + } + + protected void setCodeTypeFilters(boolean instructions, boolean data, boolean undefinedData) { + settings = settings.withIncludeInstructions(instructions); + settings = settings.withIncludeDefinedData(data); + settings = settings.withIncludeUndefinedData(undefinedData); + runSwing(() -> searchProvider.setSettings(settings)); + } +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchAsciiTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchAsciiTest.java new file mode 100644 index 0000000000..0d64b42f3d --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchAsciiTest.java @@ -0,0 +1,342 @@ +/* ### + * 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.features.base.memsearch; + +import static org.junit.Assert.*; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.test.ToyProgramBuilder; + +/** + * Tests for searching memory for ascii. + */ +public class MemSearchAsciiTest extends AbstractMemSearchTest { + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + setSearchFormat(SearchFormat.STRING); + } + + @Override + protected Program buildProgram() throws Exception { + ToyProgramBuilder builder = new ToyProgramBuilder("Test", false, ProgramBuilder._TOY); + + builder.createMemory(".text", "0x1001000", 0x6600); + builder.createMemory(".data", "0x1008000", 0x600); + builder.createMemory(".rsrc", "0x100a000", 0x5400); + //create some strings + builder.createEncodedString("0x010016ec", "something", StandardCharsets.UTF_16LE, true); + builder.createEncodedString("0x01001708", "Notepad", StandardCharsets.UTF_16LE, true); + builder.createEncodedString("0x01001740", "something else", StandardCharsets.UTF_16LE, + true); + builder.createEncodedString("0x01001840", "\u039d\u03bf\u03c4\u03b5\u03c0\u03b1\u03bd", + StandardCharsets.UTF_16LE, true); + builder.createEncodedString("0x0100186a", + "\u03c1\u03b8\u03c4\u03b5\u03c0\u03b1\u03bd\u03c2\u03b2", StandardCharsets.UTF_16LE, + true); + builder.createEncodedString("0x0100196a", + "\u03c1\u03b8\u03c4\u03b5\u03c0\u03b1\u03bd\u03c2\u03b2", StandardCharsets.UTF_8, true); + builder.createEncodedString("0x0100189d", "\"Hello world!\"\n\t-new programmer", + StandardCharsets.US_ASCII, true); + builder.createEncodedString("0x0100198e", "\"Hello world!\"\n\t-new programmer", + StandardCharsets.UTF_16LE, true); + builder.createEncodedString("0x010013cc", "notepad.exe", StandardCharsets.US_ASCII, false); + builder.createEncodedString("0x010013e0", "notepad.exe", StandardCharsets.US_ASCII, false); + builder.createEncodedString("0x1006c6a", "GetLocaleInfoW", StandardCharsets.US_ASCII, + false); + builder.createEncodedString("0x1006f26", "GetCPInfo", StandardCharsets.US_ASCII, false); + builder.createEncodedString("0x0100dde0", "NOTEPAD.EXE", StandardCharsets.UTF_16LE, true); + builder.createEncodedString("0x0100eb90", + "This string contains notepad twice. Here is the second NotePad.", + StandardCharsets.UTF_16LE, true); + builder.createEncodedString("0x0100ed00", "Another string", StandardCharsets.UTF_16LE, + true); + + return builder.getProgram(); + } + + @Test + public void testCaseSensitiveOff() throws Exception { + + setCaseSensitive(false); + + setInput("notepad"); + + List
addrs = addrs(0x010013cc, 0x010013e0); + + performSearchNext(addrs); + } + + @Test + public void testCaseSensitiveOn() throws Exception { + + setCaseSensitive(true); + + setInput("NOTEpad"); + + performSearchNext(Collections.emptyList()); + } + + @Test + public void testUnicodeNotCaseSensitive() throws Exception { + + setCaseSensitive(false); + setCharset(StandardCharsets.UTF_16); + setInput("NOTEpad"); + + List
addrs = addrs(0x01001708, 0x0100dde0, 0x0100eb90, 0x0100eb90); // this code unit contains two notepads in one string + + performSearchNext(addrs); + } + + @Test + public void testGreekUnicodeSearch() throws Exception { + setCaseSensitive(false); + setCharset(StandardCharsets.UTF_16); + setInput("\u03c4\u03b5\u03c0\u03b1\u03bd"); + + List
addrs = addrs(0x01001840, 0x0100186a); + + performSearchNext(addrs); + + setCharset(StandardCharsets.UTF_8); + + addrs = addrs(0x0100196a); + + performSearchNext(addrs); + } + + @Test + public void testRepeatUnicodeNotCaseSensitive() throws Exception { + setCaseSensitive(false); + setCharset(StandardCharsets.UTF_16); + setInput("NOTEpad"); + + performSearchNext(addr(0x01001708)); + + // this code unit contains two notepads in one string + List
addrs = addrs(0x0100dde0, 0x0100eb90, 0x0100eb90); + + for (Address address : addrs) { + repeatSearchForward(); + assertEquals(address, codeBrowser.getCurrentLocation().getAddress()); + + } + repeatSearchForward(); + assertEquals(addrs.get(2), codeBrowser.getCurrentLocation().getAddress()); + } + + @Test + public void testUnicodeCaseSensitive() throws Exception { + setCaseSensitive(true); + setCharset(StandardCharsets.UTF_16); + setInput("Notepad"); + + performSearchNext(addrs(0x01001708)); + } + + @Test + public void testSearchAllUnicodeNotCaseSensitive() throws Exception { + setCaseSensitive(false); + + setCharset(StandardCharsets.UTF_16); + setInput("NOTEpad"); + + performSearchAll(); + waitForSearch(4); + + List
addrs = addrs(0x01001708, 0x0100dde0, 0x0100ebba, 0x0100ebfe); + + checkMarkerSet(addrs); + } + + @Test + public void testSearchAllUnicodeCaseSensitive() throws Exception { + setCaseSensitive(true); + setCharset(StandardCharsets.UTF_16); + setInput("Notepad"); + + performSearchAll(); + waitForSearch(1); + + checkMarkerSet(addrs(0x01001708)); + } + + @Test + public void testSearchAllNotCaseSensitive() throws Exception { + setCaseSensitive(false); + + setInput("NOTEpad"); + performSearchAll(); + waitForSearch(2); + + List
addrs = addrs(0x010013cc, 0x010013e0); + + checkMarkerSet(addrs); + } + + @Test + public void testSearchAllCaseSensitive() throws Exception { + byte[] b = new byte[] { 'N', 'O', 'T', 'E', 'p', 'a', 'd' }; + + int transactionID = program.startTransaction("test"); + memory.setBytes(addr(0x0100b451), b); + program.endTransaction(transactionID, true); + + setCaseSensitive(true); + setInput("NOTEpad"); + performSearchAll(); + + waitForSearch(1); + + checkMarkerSet(addrs(0x0100b451)); + } + + @Test + public void testSearchAllCaseSensitiveAlign8() throws Exception { + setAlignment(8); + + setCaseSensitive(true); + + setInput("notepad"); + performSearchAll(); + waitForSearch(1); + + checkMarkerSet(addrs(0x010013e0)); + } + + @Test + public void testSearchSelection() throws Exception { + + makeSelection(tool, program, addr(0x01006c73), addr(0x01006f02)); + + assertSearchSelectionSelected(); + + setCaseSensitive(false); + + setInput("Info"); + + performSearchNext(addrs(0x01006c6a)); + } + + @Test + public void testSearchNonContiguousSelection() throws Exception { + + makeSelection(tool, program, range(0x01006c70, 0x01006c80), range(0x01006f2b, 0x01006f37)); + + assertSearchSelectionSelected(); + + setCaseSensitive(false); + + setInput("Info"); + + List
addrs = addrs(0x01006c6a, 0x01006f26); + + performSearchNext(addrs); + } + + @Test + public void testSearchBackward() throws Exception { + + goTo(tool, program, addr(0x1006f56)); + + setCaseSensitive(true); + + setInput("Info"); + + List
addrs = addrs(0x01006f26, 0x01006c6a); + + performSearchPrevious(addrs); + } + + @Test + public void testSearchBackwardInSelection() throws Exception { + + goTo(tool, program, addr(0x01006f02)); + + makeSelection(tool, program, addr(0x01006c73), addr(0x01006f02)); + + assertSearchSelectionSelected(); + + setCaseSensitive(false); + + setInput("Info"); + + List
addrs = addrs(0x01006c6a); + + performSearchPrevious(addrs); + } + + @Test + public void testSearchBackwardAlign4() throws Exception { + + goTo(tool, program, addr(0x1006f56)); + + setCaseSensitive(true); + + setAlignment(8); + + setInput("notepad"); + + List
addrs = addrs(0x010013e0); + + performSearchPrevious(addrs); + } + + @Test + public void testSearchBackwardAlign4NoneFound() throws Exception { + + goTo(tool, program, addr(0x1006f56)); + + setCaseSensitive(true); + + setAlignment(8); + + setInput("Info"); + + performSearchPrevious(Collections.emptyList()); + } + + @Test + public void testSearchEscapeSequences() throws Exception { + setCaseSensitive(true); + setEscapeSequences(true); + + setCharset(StandardCharsets.US_ASCII); + setInput("\"Hello world!\"\\n\\t-new programmer"); + + List
addrs = addrs(0x0100189d); + performSearchNext(addrs); + + setBigEndian(false); + setCharset(StandardCharsets.UTF_16); + + addrs = addrs(0x0100198e); + performSearchNext(addrs); + } +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchBinaryTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchBinaryTest.java new file mode 100644 index 0000000000..5a1993e7ba --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchBinaryTest.java @@ -0,0 +1,258 @@ +/* ### + * 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.features.base.memsearch; + +import static org.junit.Assert.*; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.Pointer32DataType; +import ghidra.program.model.listing.Program; + +/** + * Tests for the Binary format in searching memory. + */ +public class MemSearchBinaryTest extends AbstractMemSearchTest { + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + setSearchFormat(SearchFormat.BINARY); + } + + @Override + protected Program buildProgram() throws Exception { + ProgramBuilder builder = new ProgramBuilder("TestX86", ProgramBuilder._X86); + builder.createMemory(".text", Long.toHexString(0x1001000), 0x6600); + builder.createMemory(".data", Long.toHexString(0x1008000), 0x600); + builder.createMemory(".rsrc", Long.toHexString(0x100A000), 0x5400); + builder.createMemory(".bound_import_table", Long.toHexString(0xF0000248), 0xA8); + builder.createMemory(".debug_data", Long.toHexString(0xF0001300), 0x1C); + + //create and disassemble a function + builder.setBytes( + "0x01002cf5", + "55 8b ec 83 7d 14 00 56 8b 35 e0 10 00 01 57 74 09 ff 75 14 ff d6 8b f8 eb 02 33 " + + "ff ff 75 10 ff d6 03 c7 8d 44 00 02 50 6a 40 ff 15 dc 10 00 01 8b f0 85 f6 74 27 " + + "56 ff 75 14 ff 75 10 e8 5c ff ff ff ff 75 18 ff 75 0c 56 ff 75 08 ff 15 04 12 00 " + + "01 56 8b f8 ff 15 c0 10 00 01 eb 14 ff 75 18 ff 75 0c ff 75 10 ff 75 08 ff 15 04 " + + "12 00 01 8b f8 8b c7 5f 5e 5d c2 14"); + builder.disassemble("0x01002cf5", 0x121, true); + builder.createFunction("0x01002cf5"); + + //create some data + + builder.setBytes("0x1001004", "85 4f dc 77"); + builder.applyDataType("0x1001004", new Pointer32DataType(), 1); + builder.createEncodedString("0x01001708", "Notepad", StandardCharsets.UTF_16BE, true); + builder.createEncodedString("0x01001740", "something else", StandardCharsets.UTF_16BE, + true); + builder.createEncodedString("0x010013cc", "notepad.exe", StandardCharsets.US_ASCII, true); + + //create some undefined data + builder.setBytes("0x1001500", "4e 00 65 00 77 00"); + builder.setBytes("0x1003000", "55 00"); + + return builder.getProgram(); + } + + @Test + public void testBinaryInvalidEntry() { + // enter a non-binary digit; the search field should not accept it + setInput("2"); + + assertEquals("", getInput()); + } + + @Test + public void testBinaryEnterSpaces() { + // verify that more than 8 digits are allowed if spaces are entered + setInput("01110000 01110000"); + assertEquals("01110000 01110000", getInput()); + } + + @Test + public void testBinaryPasteNumberWithPrefix() { + // paste a number with a binary prefix; + // the prefix should be removed before the insertion + setInput("0b00101010"); + assertEquals("00101010", getInput()); + + setInput("0B1010 10"); + assertEquals("1010 10", getInput()); + } + + @Test + public void testBinarySearch() throws Exception { + + goTo(0x01001000); + + setInput("00010100 11111111"); + + performSearchNext(addr(0x01002d06)); + + } + + @Test + public void testBinarySearchNext() throws Exception { + + goTo(0x01001000); + + setInput("01110101"); + + //@formatter:off + List
addrs = addrs(0x01002d06, + 0x01002d11, + 0x01002d2c, + 0x01002d2f, + 0x01002d37, + 0x01002d3a, + 0x01002d3e, + 0x01002d52, + 0x01002d55, + 0x01002d58, + 0x01002d5b); + //@formatter:on + + performSearchNext(addrs); + } + + @Test + public void testBinarySearchNextAlign4() throws Exception { + goTo(0x01001000); + setInput("01110101"); + + setAlignment(4); + + //the bytes are at the right alignment value but the code units are not + List
addrs = addrs(0x01002d2f, 0x01002d37, 0x01002d5b); + performSearchNext(addrs); + } + + @Test + public void testBinarySearchAll() throws Exception { + setInput("11110110"); + + performSearchAll(); + waitForSearch(1); + + checkMarkerSet(addrs(0x1002d28)); + } + + @Test + public void testBinarySearchAllAlign4() throws Exception { + setInput("11111111 01110101"); + + setAlignment(4); + + performSearchAll(); + waitForSearch(2); + + List
startList = addrs(0x1002d2c, 0x1002d58); + + checkMarkerSet(startList); + } + + @Test + public void testCodeUnitScope_DefinedData() throws Exception { + // + // Turn on Defined Data scope and make sure only that scope yields matches + // + goTo(0x1001000);// start of program; pointer data + + setCodeTypeFilters(false, true, false); + + setInput("10000101"); + performSearchNext(addr(0x1001004)); + + // Turn off Defined Data scope and make sure we have no match at the expected address + goTo(0x1001000);// start of program; pointer data + setCodeTypeFilters(true, false, true); + performSearchNext(addr(0x1002d27)); // this is in an instruction past the data match + + // try backwards + goTo(0x1002000); + setCodeTypeFilters(false, true, false); + + performSearchPrevious(addrs(0x1001004)); + } + + @Test + public void testCodeUnitScope_UndefinedData() throws Exception { + // + // Turn on Undefined Data scope and make sure only that scope yields matches + // + goTo(0x1001000); + setCodeTypeFilters(false, false, true); + + setInput("01100101"); + performSearchNext(addr(0x1001502)); + + // Turn off Undefined Data scope and make sure we have no match at the expected address + goTo(0x1001500); + setCodeTypeFilters(true, true, false); + performSearchNext(addr(0x1001708)); + // try backwards + + goTo(0x1003000); + setCodeTypeFilters(false, false, true); + + performSearchPrevious(addrs(0x1001502)); + } + + @Test + public void testBinarySearchPrevious() throws Exception { + goTo(0x01002d6b); + + setInput("11111111 00010101"); + + List
addrs = addrs(0x01002d5e, 0x01002d4a, 0x01002d41, 0x01002d1f); + + performSearchPrevious(addrs); + } + + @Test + public void testBinaryWildcardSearch() throws Exception { + goTo(0x01001000); + + setInput("010101xx 10001011"); + + List
addrs = addrs(0x01002cf5, 0x01002cfc, 0x01002d47); + + performSearchNext(addrs); + } + + @Test + public void testBinaryWildcardSearchAll() throws Exception { + + setInput("10001011 1111xxxx"); + performSearchAll(); + waitForSearch(4); + + List
addrs = addrs(0x1002d0b, 0x1002d25, 0x1002d48, 0x1002d64); + + checkMarkerSet(addrs); + } + +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchHexTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchHexTest.java similarity index 60% rename from Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchHexTest.java rename to Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchHexTest.java index c944729927..16806af7b9 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchHexTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchHexTest.java @@ -4,36 +4,32 @@ * 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.plugin.core.searchmem; +package ghidra.features.base.memsearch; import static org.junit.Assert.*; -import java.awt.Component; -import java.awt.Container; import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.List; -import javax.swing.*; -import javax.swing.border.Border; -import javax.swing.border.TitledBorder; - import org.junit.Before; import org.junit.Test; import docking.widgets.fieldpanel.support.Highlight; import ghidra.GhidraOptions; -import ghidra.app.plugin.core.table.TableComponentProvider; import ghidra.app.services.MarkerSet; import ghidra.app.util.viewer.field.BytesFieldFactory; +import ghidra.features.base.memsearch.bytesource.ProgramSearchRegion; +import ghidra.features.base.memsearch.format.SearchFormat; import ghidra.framework.options.Options; import ghidra.program.database.ProgramBuilder; import ghidra.program.model.address.Address; @@ -50,11 +46,11 @@ public class MemSearchHexTest extends AbstractMemSearchTest { super(); } - @Override @Before + @Override public void setUp() throws Exception { super.setUp(); - selectRadioButton("Hex"); + setSearchFormat(SearchFormat.HEX); } @Override @@ -115,98 +111,43 @@ public class MemSearchHexTest extends AbstractMemSearchTest { @Test public void testDisplayDialog() throws Exception { assertTrue(searchAction.isEnabled()); - - // dig up the components of the dialog - dialog = waitForDialogComponent(MemSearchDialog.class); - assertNotNull(dialog); - assertNotNull(valueComboBox); - assertNotNull(hexLabel); - - assertButtonState("Hex", true, true); - assertButtonState("String", true, false); - assertButtonState("Decimal", true, false); - assertButtonState("Binary", true, false); - assertButtonState("Regular Expression", true, false); - assertButtonState("Little Endian", true, true); - assertButtonState("Big Endian", true, false); - assertButtonState("Search Selection", false, false); - - assertEnabled("Next", false); - assertEnabled("Previous", false); - assertEnabled("Search All", false); - assertEnabled("Dismiss", true); - - JPanel p = findTitledJPanel(pane, "Format Options"); - assertNotNull(p); - assertTrue(p.isVisible()); + assertTrue(searchProvider.isVisible()); } @Test public void testHexInvalidEntry() { // enter a non-hex digit; the search field should not accept it - setValueText("z"); + setInput("z"); - assertEquals("", valueField.getText()); + assertEquals("", getInput()); } @Test public void testHexEnterSpaces() { // verify that more than 16 digits are allowed if spaces are entered - setValueText("01 23 45 67 89 a b c d e f 1 2 3"); - assertEquals("01 23 45 67 89 a b c d e f 1 2 3", valueField.getText()); + setInput("01 23 45 67 89 a b c d e f 1 2 3"); + assertEquals("01 23 45 67 89 a b c d e f 1 2 3", getInput()); } - @Test - public void testHexNoSpaces() { - // enter a hex sequence (no spaces) more than 2 digits; - // the hex label should display the bytes reversed - setValueText("012345678"); - assertEquals("78 56 34 12 00 ", hexLabel.getText()); - } - - @Test - public void testHexBigLittleEndian() throws Exception { - // switch between little and big endian; - // verify the hex label - setValueText("012345678"); - - pressButtonByText(pane, "Big Endian", true); - - waitForSwing(); - assertEquals("00 12 34 56 78 ", hexLabel.getText()); - } - - @Test - public void testHexSpaceBetweenBytes() throws Exception { - // enter a hex sequence where each byte is separated by a space; - // ensure that the byte order setting has no effect on the sequence - setValueText("01 23 45 67 89"); - assertEquals("01 23 45 67 89", valueField.getText()); - - pressButtonByText(pane, "Big Endian", true); - - assertEquals("01 23 45 67 89", valueField.getText()); - } - @Test public void testHexPasteNumberWithPrefixAndSuffix() { // paste a number with a hex prefix; // the prefix should be removed before the insertion - setValueText("0xabcdef"); - assertEquals("abcdef", valueField.getText()); - - setValueText("$68000"); - assertEquals("68000", valueField.getText()); - + setInput("0xabcdef"); + assertEquals("abcdef", getInput()); + + setInput("$68000"); + assertEquals("68000", getInput()); + // same for 'h' the suffix - setValueText("ABCDEFh"); - assertEquals("ABCDEF", valueField.getText()); - + setInput("ABCDEFh"); + assertEquals("ABCDEF", getInput()); + // should also somehow work with leading and trailing white spaces - setValueText(" 0X321 "); - assertEquals("321", valueField.getText().strip()); - setValueText(" 123H "); - assertEquals("123", valueField.getText().strip()); + setInput(" 0X321 "); + assertEquals("321", getInput().strip()); + setInput(" 123H "); + assertEquals("123", getInput().strip()); } @Test @@ -216,9 +157,9 @@ public class MemSearchHexTest extends AbstractMemSearchTest { List
addrs = addrs(0x1002d06, 0x1002d2c, 0x1002d50); - setValueText("14 ff"); + setInput("14 ff"); - performSearchTest(addrs, "Next"); + performSearchNext(addrs); } @Test @@ -248,22 +189,21 @@ public class MemSearchHexTest extends AbstractMemSearchTest { ); //@formatter:on - setValueText("75"); + setInput("75"); - performSearchTest(addrs, "Next"); + performSearchNext(addrs); } @Test public void testHexContiguousSelection() throws Exception { - makeSelection(tool, program, range(0x01002cf5, 0x01002d6d)); assertSearchSelectionSelected(); - setValueText("50"); + setInput("50"); - performSearchTest(addrs(0x01002d1c), "Next"); + performSearchNext(addrs(0x01002d1c)); } @Test @@ -273,11 +213,11 @@ public class MemSearchHexTest extends AbstractMemSearchTest { assertSearchSelectionSelected(); - setValueText("50"); + setInput("50"); List
addrs = addrs(0x01002d1c, 0x01004120, 0x01004200, 0x01004247); - performSearchTest(addrs, "Next"); + performSearchNext(addrs); } @Test @@ -290,17 +230,14 @@ public class MemSearchHexTest extends AbstractMemSearchTest { makeSelection(tool, program, range(0x01002cf5, 0x01002d6d), range(0x01004100, 0x010041ff)); - // select Search All option to turn off searching only in selection - assertButtonState("Search All", true, false); - - // Note: this is 'Search All' for the search type, not the JButton on the button panel - pressButtonByText(pane, "Search All"); + assertSearchSelectionSelected(); + setSearchSelectionOnly(false); List
addrs = addrs(0x01002d1c, 0x01004120, 0x01004200, 0x01004247); - setValueText("50"); + setInput("50"); - performSearchTest(addrs, "Next"); + performSearchNext(addrs); } @Test @@ -309,10 +246,11 @@ public class MemSearchHexTest extends AbstractMemSearchTest { // test the marker stuff goTo(0x1004180); - setValueText("50"); - pressSearchAllButton(); + setInput("50"); - waitForSearch("Search Memory - ", 4); + performSearchAll(); + + waitForSearch(4); List
addrs = addrs(0x01002d1c, 0x01004120, 0x01004200, 0x01004247); @@ -322,11 +260,11 @@ public class MemSearchHexTest extends AbstractMemSearchTest { @Test public void testHexSearchAll2() throws Exception { // enter search string for multiple byte match - // ff 15 - setValueText("ff 15"); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 5); + setInput("ff 15"); + performSearchAll(); + + waitForSearch(5); List
addrs = addrs(0x01002d1f, 0x01002d41, 0x01002d4a, 0x01002d5e, 0x010029bd); @@ -335,16 +273,11 @@ public class MemSearchHexTest extends AbstractMemSearchTest { @Test public void testHexSearchAllAlign8() throws Exception { - // QueryResults should get displayed - // test the marker stuff + setAlignment(8); + setInput("8b"); + performSearchAll(); - JTextField alignment = (JTextField) findComponentByName(dialog.getComponent(), "Alignment"); - setText(alignment, "8"); - - setValueText("8b"); - pressSearchAllButton(); - - waitForSearch("Search Memory - ", 1); + waitForSearch(1); checkMarkerSet(addrs(0x01002d48)); } @@ -352,11 +285,11 @@ public class MemSearchHexTest extends AbstractMemSearchTest { @Test public void testHexHighlight() throws Exception { - setValueText("80 00 01"); + setInput("80 00 01"); - pressSearchAllButton(); + performSearchAll(); - waitForSearch("Search Memory - ", 1); + waitForSearch(1); Highlight[] h = getByteHighlights(addr(0x10040d9), "8b 0d 58 80 00 01"); assertEquals(1, h.length); @@ -366,10 +299,10 @@ public class MemSearchHexTest extends AbstractMemSearchTest { @Test public void testHexHighlight2() throws Exception { - setValueText("01 8b"); - pressSearchAllButton(); + setInput("01 8b"); + performSearchAll(); - waitForSearch("Search Memory - ", 3); + waitForSearch(3); Highlight[] h = getByteHighlights(addr(0x10029bd), "ff 15 d4 10 00 01"); assertEquals(1, h.length); @@ -379,10 +312,10 @@ public class MemSearchHexTest extends AbstractMemSearchTest { @Test public void testHexHighlight3() throws Exception { - setValueText("d8 33 f6 3b"); - pressSearchAllButton(); + setInput("d8 33 f6 3b"); + performSearchAll(); - waitForSearch("Search Memory - ", 1); + waitForSearch(1); Highlight[] h = getByteHighlights(addr(0x10029c3), "8b d8"); assertEquals(1, h.length); @@ -393,10 +326,10 @@ public class MemSearchHexTest extends AbstractMemSearchTest { @Test public void testHexHighlight4() throws Exception { - setValueText("fd ff ff"); - pressSearchAllButton(); + setInput("fd ff ff"); + performSearchAll(); - waitForSearch("Search Memory - ", 1); + waitForSearch(1); Highlight[] h = getByteHighlights(addr(0x10035f8), "b9 30 fd ff ff"); assertEquals(1, h.length); @@ -410,10 +343,10 @@ public class MemSearchHexTest extends AbstractMemSearchTest { opt.setInt(BytesFieldFactory.BYTE_GROUP_SIZE_MSG, 3); opt.setString(BytesFieldFactory.DELIMITER_MSG, "#@#"); - setValueText("fd ff ff"); - pressSearchAllButton(); + setInput("fd ff ff"); + performSearchAll(); - waitForSearch("Search Memory - ", 1); + waitForSearch(1); Highlight[] h = getByteHighlights(addr(0x10035f8), "b930fd#@#ffff"); assertEquals(1, h.length); @@ -424,22 +357,19 @@ public class MemSearchHexTest extends AbstractMemSearchTest { @Test public void testMarkersRemoved() throws Exception { - setValueText("ff 15"); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 5); + setInput("ff 15"); + performSearchAll(); + waitForSearch(5); - List
startList = addrs(0x01002d1f, 0x01002d41, 0x01002d4a, 0x01002d5e, 0x010029bd); + String title = searchProvider.getTitle(); - checkMarkerSet(startList); - - TableComponentProvider[] providers = tableServicePlugin.getManagedComponents(); - assertEquals(1, providers.length); - assertTrue(tool.isVisible(providers[0])); + MarkerSet markerSet = markerService.getMarkerSet(title, program); + assertNotNull(markerSet); //close it - runSwing(() -> providers[0].closeComponent()); + runSwing(() -> searchProvider.closeComponent()); - MarkerSet markerSet = markerService.getMarkerSet("Memory Search Results", program); + markerSet = markerService.getMarkerSet("Memory Search Results", program); assertNull(markerSet); } @@ -448,11 +378,9 @@ public class MemSearchHexTest extends AbstractMemSearchTest { goTo(0x01001000); - setValueText("75"); - pressButtonByText(pane, "Previous"); - waitForSearchTask(); + setInput("75"); - assertEquals("Not Found", getStatusText()); + performSearchPrevious(Collections.emptyList()); } @Test @@ -463,11 +391,11 @@ public class MemSearchHexTest extends AbstractMemSearchTest { //start at 1002d6d and search backwards goTo(0x1002d6d); - setValueText("ff 15"); + setInput("ff 15"); List
addrs = addrs(0x01002d5e, 0x01002d4a, 0x01002d41, 0x01002d1f, 0x010029bd); - performSearchTest(addrs, "Previous"); + performSearchPrevious(addrs); } @Test @@ -477,13 +405,13 @@ public class MemSearchHexTest extends AbstractMemSearchTest { goTo(0x1002d6d); - setAlignment("2"); + setAlignment(2); - setValueText("ff 15"); + setInput("ff 15"); List
addrs = addrs(0x01002d5e, 0x01002d4a); - performSearchTest(addrs, "Previous"); + performSearchPrevious(addrs); } @Test @@ -495,9 +423,9 @@ public class MemSearchHexTest extends AbstractMemSearchTest { assertSearchSelectionSelected(); - setValueText("50"); + setInput("50"); - performSearchTest(addrs(0x01002d1c), "Previous"); + performSearchPrevious(addrs(0x01002d1c)); } @Test @@ -511,9 +439,9 @@ public class MemSearchHexTest extends AbstractMemSearchTest { List
addrs = addrs(0x01004247, 0x01004200, 0x01004120, 0x01002d1c); - setValueText("50"); + setInput("50"); - performSearchTest(addrs, "Previous"); + performSearchPrevious(addrs); } @Test @@ -522,9 +450,9 @@ public class MemSearchHexTest extends AbstractMemSearchTest { List
addrs = addrs(0x01002d0b, 0x01002d25, 0x01002d48, 0x01002d64); - setValueText("8b f?"); + setInput("8b f?"); - performSearchTest(addrs, "Next"); + performSearchNext(addrs); } @Test @@ -539,9 +467,9 @@ public class MemSearchHexTest extends AbstractMemSearchTest { List
addrs = addrs(0x01001004, 0x01002d27); - setValueText("85 ?"); + setInput("85 ?"); - performSearchTest(addrs, "Next"); + performSearchNext(addrs); } @Test @@ -556,9 +484,9 @@ public class MemSearchHexTest extends AbstractMemSearchTest { List
addrs = addrs(0x01001004, 0x01002d27); - setValueText("85 ."); + setInput("85 ."); - performSearchTest(addrs, "Next"); + performSearchNext(addrs); } @Test @@ -568,9 +496,9 @@ public class MemSearchHexTest extends AbstractMemSearchTest { List
addrs = addrs(0x01002d64, 0x01002d48, 0x01002d25, 0x01002d0b); - setValueText("8b f?"); + setInput("8b f?"); - performSearchTest(addrs, "Previous"); + performSearchPrevious(addrs); } @Test @@ -578,9 +506,9 @@ public class MemSearchHexTest extends AbstractMemSearchTest { // QueryResults should get displayed // test the marker stuff - setValueText("8b f?"); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 4); + setInput("8b f?"); + performSearchAll(); + waitForSearch(4); List
addrs = addrs(0x01002d64, 0x01002d48, 0x01002d25, 0x01002d0b); @@ -589,96 +517,39 @@ public class MemSearchHexTest extends AbstractMemSearchTest { @Test public void testHexByteOrder() throws Exception { - selectRadioButton("Big Endian"); + setBigEndian(true); goTo(0x01001000); - setValueText("8bec"); + setInput("8bec"); - performSearchTest(addrs(0x01002cf6), "Next"); + performSearchNext(addrs(0x01002cf6)); } @Test public void testSearchInOtherSpace() throws Exception { goTo(0x01001000); - setValueText("01 02 03 04 05 06 07 08 09"); + setInput("01 02 03 04 05 06 07 08 09"); - selectRadioButton("All Blocks"); + addSearchRegion(ProgramSearchRegion.OTHER, true); List
addrs = addrs(program.getAddressFactory().getAddress("otherOverlay:1")); - performSearchTest(addrs, "Next"); + performSearchNext(addrs); } @Test - public void testValueComboBox() throws Exception { - setValueText("00 65"); + public void testRepeatSearchForwardThenBackwards() throws Exception { - pressSearchButton("Next"); - setValueText(""); + setInput("8b f8"); + performSearchNext(addr(0x01002d0b)); - setValueText("d1 e1"); - pressSearchButton("Next"); - setValueText(""); - - setValueText("0123456"); - pressSearchButton("Next"); - setValueText(""); - - // the combo box should list most recently entered values - DefaultComboBoxModel cbModel = (DefaultComboBoxModel) valueComboBox.getModel(); - assertEquals(3, cbModel.getSize()); - assertEquals("0123456", cbModel.getElementAt(0)); - assertEquals("d1 e1", cbModel.getElementAt(1)); - assertEquals("00 65", cbModel.getElementAt(2)); - } - - @Test - public void testRepeatSearchAction() throws Exception { - - setValueText("8b f8"); - pressSearchButton("Next"); - - assertEquals(addr(0x01002d0b), currentAddress()); - - repeatSearch(); + repeatSearchForward(); assertEquals(addr(0x01002d48), currentAddress()); + + repeatSearchBackward(); + + assertEquals(addr(0x01002d0b), currentAddress()); } - - @Test - public void testSearchBackwardsWhenAtFirstAddressWithCurrentMatch() throws Exception { - setValueText("00"); - - pressSearchButton("Next"); - pressSearchButton("Previous"); - pressSearchButton("Previous"); - - assertEquals("Not Found", getStatusText()); - } - -//================================================================================================== -// Private Methods -//================================================================================================== - - private JPanel findTitledJPanel(Container container, String title) { - if (container instanceof JPanel) { - JPanel p = (JPanel) container; - Border b = p.getBorder(); - if ((b instanceof TitledBorder) && ((TitledBorder) b).getTitle().equals(title)) { - return p; - } - } - Component[] comps = container.getComponents(); - for (Component element : comps) { - if (element instanceof Container) { - JPanel p = findTitledJPanel((Container) element, title); - if (p != null) { - return p; - } - } - } - return null; - } - } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchNumbersTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchNumbersTest.java new file mode 100644 index 0000000000..202af6a4b2 --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchNumbersTest.java @@ -0,0 +1,473 @@ +/* ### + * 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.features.base.memsearch; + +import static org.junit.Assert.*; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.Pointer32DataType; +import ghidra.program.model.listing.Program; + +/** + * Tests for searching for decimal values in memory. + */ +public class MemSearchNumbersTest extends AbstractMemSearchTest { + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + setSearchFormat(SearchFormat.DECIMAL); + setDecimalSize(4); + } + + @Override + protected Program buildProgram() throws Exception { + ProgramBuilder builder = new ProgramBuilder("TestX86", ProgramBuilder._X86); + builder.createMemory(".text", Long.toHexString(0x1001000), 0x6600); + builder.createMemory(".data", Long.toHexString(0x1008000), 0x600); + builder.createMemory(".rsrc", Long.toHexString(0x100A000), 0x5400); + builder.createMemory(".bound_import_table", Long.toHexString(0xF0000248), 0xA8); + builder.createMemory(".debug_data", Long.toHexString(0xF0001300), 0x1C); + + //create and disassemble a function + builder.setBytes( + "0x01002cf5", + "55 8b ec 83 7d 14 00 56 8b 35 e0 10 00 01 57 74 09 ff 75 14 ff d6 8b f8 eb 02 " + + "33 ff ff 75 10 ff d6 03 c7 8d 44 00 02 50 6a 40 ff 15 dc 10 00 01 8b f0 85 f6 " + + "74 27 56 ff 75 14 ff 75 10 e8 5c ff ff ff ff 75 18 ff 75 0c 56 ff 75 08 ff 15 " + + "04 12 00 01 56 8b f8 ff 15 c0 10 00 01 eb 14 ff 75 18 ff 75 0c ff 75 10 ff 75 " + + "08 ff 15 04 12 00 01 8b f8 8b c7 5f 5e 5d c2 14"); + builder.disassemble("0x01002cf5", 0x121, true); + builder.createFunction("0x01002cf5"); + + //create some data + + builder.setBytes("0x1001004", "85 4f dc 77"); + builder.applyDataType("0x1001004", new Pointer32DataType(), 1); + builder.createEncodedString("0x01001708", "Notepad", StandardCharsets.UTF_16BE, true); + builder.createEncodedString("0x01001740", "something else", StandardCharsets.UTF_16BE, + true); + builder.createEncodedString("0x010013cc", "notepad.exe", StandardCharsets.US_ASCII, false); + + //create some undefined data + builder.setBytes("0x1001500", "4e 00 65 00 77 00"); + builder.setBytes("0x1003000", "55 00"); + builder.setBytes("0x1004100", "64 00 00 00");//100 dec + builder.setBytes("0x1004120", "50 ff 75 08");//7.4027124e-34 float + builder.setBytes("0x1004135", "64 00 00 00");//100 dec + builder.setBytes("0x1004200", "50 ff 75 08 e8 8d 3c 00");//1.588386874245921e-307 + builder.setBytes("0x1004247", "50 ff 75 08");//7.4027124e-34 float + builder.setBytes("0x1004270", "65 00 6e 00 64 00 69 00");//29555302058557541 qword + + return builder.getProgram(); + } + + @Test + public void testInvalidEntry() throws Exception { + // enter non-numeric value + setInput("z"); + assertEquals("", getInput()); + } + + @Test + public void testValueTooLarge() throws Exception { + setDecimalSize(1); + + setInput("262"); + assertEquals("", getInput()); + } + + @Test + public void testNegativeValueEntered() throws Exception { + // enter a negative value; the hexLabel should show the correct + // byte sequence + setSearchFormat(SearchFormat.DECIMAL); + setDecimalSize(2); + setInput("-1234"); + assertEquals("2e fb", getByteString()); + + setDecimalSize(1); + assertEquals("46 -5", getInput()); + + setInput("-55"); + assertEquals("c9", getByteString()); + + setDecimalSize(4); + assertEquals("-55", getInput()); + assertEquals("c9 ff ff ff", getByteString()); + + setDecimalSize(8); + assertEquals("-55", getInput()); + assertEquals("c9 ff ff ff ff ff ff ff", getByteString()); + + setSearchFormat(SearchFormat.FLOAT); + assertEquals("00 00 5c c2", getByteString()); + + setSearchFormat(SearchFormat.DOUBLE); + assertEquals("00 00 00 00 00 80 4b c0", getByteString()); + } + + @Test + public void testMulipleValuesEntered() throws Exception { + // enter values separated by a space; values should be accepted + setDecimalSize(1); + setInput("12 34 56 78"); + assertEquals("0c 22 38 4e", getByteString()); + + setDecimalSize(2); + assertEquals("8716 20024", getInput()); + assertEquals("0c 22 38 4e", getByteString()); + + setDecimalSize(4); + assertEquals("1312301580", getInput()); + assertEquals("0c 22 38 4e", getByteString()); + + setDecimalSize(8); + assertEquals("1312301580", getInput()); + assertEquals("0c 22 38 4e 00 00 00 00", getByteString()); + + setSearchFormat(SearchFormat.FLOAT); + assertEquals("1312301580", getInput()); + assertEquals("44 70 9c 4e", getByteString()); + + setSearchFormat(SearchFormat.DOUBLE); + assertEquals("1312301580", getInput()); + assertEquals("00 00 00 83 08 8e d3 41", getByteString()); + + } + + @Test + public void testByteOrder() throws Exception { + setBigEndian(true); + setInput("12 34 56 78"); + setDecimalSize(1); + setBigEndian(true); + // should be unaffected + assertEquals("0c 22 38 4e", getByteString()); + + setDecimalSize(2); + assertEquals("3106 14414", getInput()); + assertEquals("0c 22 38 4e", getByteString()); + + setDecimalSize(4); + assertEquals("203569230", getInput()); + assertEquals("0c 22 38 4e", getByteString()); + + setDecimalSize(8); + assertEquals("203569230", getInput()); + assertEquals("00 00 00 00 0c 22 38 4e", getByteString()); + + setSearchFormat(SearchFormat.FLOAT); + assertEquals("203569230", getInput()); + assertEquals("4d 42 23 85", getByteString()); + + setSearchFormat(SearchFormat.DOUBLE); + assertEquals("203569230", getInput()); + assertEquals("41 a8 44 70 9c 00 00 00", getByteString()); + } + + @Test + public void testFloatDoubleFormat() throws Exception { + setSearchFormat(SearchFormat.FLOAT); + + setInput("12.345"); + assertEquals("12.345", getInput()); + assertEquals("1f 85 45 41", getByteString()); + + setSearchFormat(SearchFormat.DOUBLE); + assertEquals("71 3d 0a d7 a3 b0 28 40", getByteString()); + } + + @Test + public void testSearchByte() throws Exception { + goTo(program.getMinAddress()); + + List
addrs = addrs(0x1002d3e, 0x1002d5b, 0x1004123, 0x1004203, 0x100424a); + + setDecimalSize(1); + setInput("8"); + + performSearchNext(addrs); + } + + @Test + public void testSearchByteBackward() throws Exception { + + goTo(0x01002d6d); + + setDecimalSize(1); + + setInput("8"); + + List
addrs = addrs(0x1002d5b, 0x1002d3e); + + performSearchPrevious(addrs); + } + + @Test + public void testSearchWord() throws Exception { + + goTo(program.getMinAddress()); + + setDecimalSize(2); + + setInput("20"); + + List
addrs = addrs(0x1002cf8, 0x1002d6b); + + performSearchNext(addrs); + } + + @Test + public void testSearchWordBackward() throws Exception { + + goTo(0x01002d6e); + + setDecimalSize(2); + + setInput("20"); + + List
addrs = addrs(0x1002d6b, 0x1002cf8); + + performSearchPrevious(addrs); + } + + @Test + public void testSearchDWord() throws Exception { + goTo(program.getMinAddress()); + + setDecimalSize(4); + + setInput("100"); + + List
addrs = addrs(0x1001708, 0x1004100, 0x1004135); + + performSearchNext(addrs); + } + + @Test + public void testSearchDWordBackward() throws Exception { + goTo(0x01005000); + + setDecimalSize(4); + + setInput("100"); + + List
addrs = addrs(0x1004135, 0x1004100, 0x1001708); + + performSearchPrevious(addrs); + } + + @Test + public void testSearchQWord() throws Exception { + goTo(program.getMinAddress()); + + setDecimalSize(8); + + setInput("29555302058557541"); + + performSearchNext(addrs(0x1004270)); + } + + @Test + public void testSearchQWordBackward() throws Exception { + + goTo(program.getMaxAddress()); + + setDecimalSize(8); + + setInput("29555302058557541"); + + performSearchPrevious(addrs(0x1004270)); + } + + @Test + public void testSearchFloat() throws Exception { + + goTo(program.getMinAddress()); + + setSearchFormat(SearchFormat.FLOAT); + + setInput("7.4027124e-34"); + + List
addrs = addrs(0x1004120, 0x1004200, 0x1004247); + + performSearchNext(addrs); + } + + @Test + public void testSearchFloatBackward() throws Exception { + + goTo(0x01005000); + + setSearchFormat(SearchFormat.FLOAT); + + setInput("7.4027124e-34"); + + List
addrs = addrs(0x1004247, 0x1004200, 0x1004120); + + performSearchPrevious(addrs); + } + + @Test + public void testSearchFloatBackwardAlign8() throws Exception { + + goTo(program.getMaxAddress()); + + setAlignment(8); + setSearchFormat(SearchFormat.FLOAT); + + setInput("7.4027124e-34"); + + List
addrs = addrs(0x1004200, 0x1004120); + + performSearchPrevious(addrs); + } + + @Test + public void testSearchDouble() throws Exception { + + goTo(program.getMinAddress()); + + setSearchFormat(SearchFormat.DOUBLE); + + setInput("1.588386874245921e-307"); + + List
addrs = addrs(0x1004200); + + performSearchNext(addrs); + } + + @Test + public void testSearchDoubleBackward() throws Exception { + + goTo(program.getMaxAddress()); + + setSearchFormat(SearchFormat.DOUBLE); + + setInput("1.588386874245921e-307"); + + List
addrs = addrs(0x1004200); + + performSearchPrevious(addrs); + } + + @Test + public void testSearchAllByte() throws Exception { + + setDecimalSize(1); + + setInput("8"); + performSearchAll(); + waitForSearch(5); + + List
addrs = addrs(0x1002d40, 0x1002d5d, 0x1004123, 0x1004203, 0x100424a); + + checkMarkerSet(addrs); + } + + @Test + public void testSearchAllWord() throws Exception { + + setDecimalSize(2); + + setInput("20"); + + performSearchAll(); + waitForSearch(2); + + List
addrs = addrs(0x1002cfa, 0x1002d6c); + + checkMarkerSet(addrs); + } + + @Test + public void testSearchAllWordAlign4() throws Exception { + setAlignment(4); + + setDecimalSize(2); + + setInput("20"); + + performSearchAll(); + waitForSearch(1); + + checkMarkerSet(addrs(0x1002d6c)); + } + + @Test + public void testSearchAllDWord() throws Exception { + + setDecimalSize(4); + + setInput("100"); + performSearchAll(); + waitForSearch(3); + + List
addrs = addrs(0x1001715, 0x1004100, 0x1004135); + + checkMarkerSet(addrs); + } + + @Test + public void testSearchAllQWord() throws Exception { + + setDecimalSize(8); + + setInput("29555302058557541"); + performSearchAll(); + waitForSearch(1); + + checkMarkerSet(addrs(0x1004270)); + } + + @Test + public void testSearchAllFloat() throws Exception { + + setSearchFormat(SearchFormat.FLOAT); + + setInput("7.4027124e-34"); + + performSearchAll(); + waitForSearch(3); + + List
addrs = addrs(0x1004120, 0x1004200, 0x1004247); + + checkMarkerSet(addrs); + } + + @Test + public void testSearchAllDouble() throws Exception { + + setSearchFormat(SearchFormat.DOUBLE); + + setInput("1.588386874245921e-307"); + + performSearchAll(); + waitForSearch(1); + + checkMarkerSet(addrs(0x1004200)); + } +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchRegExTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchRegExTest.java similarity index 62% rename from Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchRegExTest.java rename to Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchRegExTest.java index 8c55fcda1e..a0133b678b 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchRegExTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchRegExTest.java @@ -4,16 +4,16 @@ * 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.plugin.core.searchmem; +package ghidra.features.base.memsearch; import static org.junit.Assert.*; @@ -24,10 +24,11 @@ import org.junit.Before; import org.junit.Test; import docking.widgets.fieldpanel.support.Highlight; +import ghidra.features.base.memsearch.format.SearchFormat; import ghidra.program.database.ProgramBuilder; import ghidra.program.model.address.Address; import ghidra.program.model.data.Pointer32DataType; -import ghidra.program.model.listing.*; +import ghidra.program.model.listing.Program; /** * Tests for searching memory for hex reg expression. @@ -38,7 +39,7 @@ public class MemSearchRegExTest extends AbstractMemSearchTest { @Before public void setUp() throws Exception { super.setUp(); - selectRadioButton("Regular Expression"); + setSearchFormat(SearchFormat.REG_EX); } @Override @@ -112,13 +113,11 @@ public class MemSearchRegExTest extends AbstractMemSearchTest { // NOTE: the following regular expression searches for 0x8b followed // by 0-10 occurrences of any character, followed by 0x56. - setValueText("\\x8b.{0,10}\\x56"); - - assertEquals("", hexLabel.getText()); + setInput("\\x8b.{0,10}\\x56"); List
addrs = addrs(0x01002cf6, 0x01002d25); - performSearchTest(addrs, "Next"); + performSearchNext(addrs); } @Test @@ -126,33 +125,18 @@ public class MemSearchRegExTest extends AbstractMemSearchTest { // // Turn on Instructions scope and make sure only that scope yields matches // - goTo(0x1002cf5); // 'ghidra' function address + goTo(0x1002cf4); // just before instruction hit + setCodeTypeFilters(true, false, false); // only accept instruction matches - selectCheckBox("Instructions", true); - selectCheckBox("Defined Data", false); - selectCheckBox("Undefined Data", false); - - setValueText("\\x55"); - pressSearchButton("Next"); - Address expectedSearchAddressHit = addr(0x1002cf5); - assertEquals( - "Did not find a hit at the next matching Instruction when we are searching Instructions", - expectedSearchAddressHit, cb.getCurrentLocation().getAddress()); + setInput("\\x55"); + performSearchNext(addr(0x1002cf5)); // Turn off Instructions scope and make sure we have no match at the expected address - goTo(0x1002cf5); // 'ghidra' function address + goTo(0x1002cf4); - selectCheckBox("Instructions", false); - selectCheckBox("Defined Data", true); - selectCheckBox("Undefined Data", true); - pressSearchButton("Next"); - assertTrue( - "Found a search match at an Instruction, even though no Instruction should be searched", - !expectedSearchAddressHit.equals(currentAddress())); + setCodeTypeFilters(false, true, true); // all but instruction matches + performSearchNext(addr(0x1003000)); // this is in undefined data - CodeUnit codeUnit = currentCodeUnit(); - assertTrue("Did not find a data match when searching instructions is disabled", - codeUnit instanceof Data); } @Test @@ -160,33 +144,18 @@ public class MemSearchRegExTest extends AbstractMemSearchTest { // // Turn on Defined Data scope and make sure only that scope yields matches // - goTo(0x1001004);// start of program; pointer data + goTo(0x1001000);// start of program + setCodeTypeFilters(false, true, false); // only accept defined data matches + setInput("\\x85"); - selectCheckBox("Instructions", false); - selectCheckBox("Defined Data", true); - selectCheckBox("Undefined Data", false); - - setValueText("\\x85"); - pressSearchButton("Next"); - Address expectedSearchAddressHit = addr(0x1001004); - assertEquals( - "Did not find a hit at the next matching Defined Data when we are searching Defined Data", - expectedSearchAddressHit, cb.getCurrentLocation().getAddress()); + performSearchNext(addr(0x1001004)); // Turn off Defined Data scope and make sure we have no match at the expected address - goTo(0x1001004);// start of program; pointer data + goTo(0x1001000);// start of program - selectCheckBox("Instructions", true); - selectCheckBox("Defined Data", false); - selectCheckBox("Undefined Data", true); - pressSearchButton("Next"); - assertTrue( - "Found a search match at a Defined Data, even though no Defined Data should be searched", - !expectedSearchAddressHit.equals(currentAddress())); + setCodeTypeFilters(true, false, true); // don't accept defined data + performSearchNext(addr(0x1002d27)); - CodeUnit codeUnit = currentCodeUnit(); - assertTrue("Did not find a instruction match when searching defined data is disabled", - codeUnit instanceof Instruction); } @Test @@ -194,33 +163,19 @@ public class MemSearchRegExTest extends AbstractMemSearchTest { // // Turn on Undefined Data scope and make sure only that scope yields matches // - goTo(0x1004270); + goTo(0x1004269); - selectCheckBox("Instructions", false); - selectCheckBox("Defined Data", false); - selectCheckBox("Undefined Data", true); + setCodeTypeFilters(false, false, true); // only accept undefined data matches + setInput("\\x65"); - setValueText("\\x65"); - pressSearchButton("Next"); - Address expectedSearchAddressHit = addr(0x1004270); - assertEquals( - "Did not find a hit at the next matching Undefined Data when we are searching Undefined Data", - expectedSearchAddressHit, cb.getCurrentLocation().getAddress()); + performSearchNext(addr(0x1004270)); // Turn off Undefined Data scope and make sure we have no match at the expected address - goTo(0x1004270); + setCodeTypeFilters(true, true, false); // don't accept undefined data + goTo(0x1004260); - selectCheckBox("Instructions", true); - selectCheckBox("Defined Data", true); - selectCheckBox("Undefined Data", false); - pressSearchButton("Next"); - assertTrue( - "Found a search match at an Undefined Data, even though no Undefined Data should be searched", - !expectedSearchAddressHit.equals(currentAddress())); + performSearchNext(addr(0x1004300)); // this is defined data past where we undefined match - CodeUnit codeUnit = currentCodeUnit(); - assertTrue("Did not find a data match when searching undefined data is disabled", - codeUnit instanceof Data); } @Test @@ -228,13 +183,11 @@ public class MemSearchRegExTest extends AbstractMemSearchTest { // NOTE: the following regular expression searches for 0x56 followed // by 0-10 occurrences of any character, followed by 0x10. - setValueText("\\x56.{0,10}\\x10"); + setInput("\\x56.{0,10}\\x10"); - assertEquals("", hexLabel.getText()); + setAlignment(4); - setAlignment("4"); - - performSearchTest(addrs(0x01002cfc), "Next"); + performSearchNext(addrs(0x01002cfc)); } @Test @@ -242,14 +195,12 @@ public class MemSearchRegExTest extends AbstractMemSearchTest { // Note: the following regular expression searches for 0x56 followed // by 0-10 occurrences of any character, followed by 0x10. - setValueText("\\x56.{0,10}\\x10"); - - assertEquals("", hexLabel.getText()); + setInput("\\x56.{0,10}\\x10"); List
addrs = addrs(0x01002cfc, 0x01002d2b, 0x01002d47); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 3); + performSearchAll(); + waitForSearch(3); checkMarkerSet(addrs); } @@ -259,13 +210,12 @@ public class MemSearchRegExTest extends AbstractMemSearchTest { // NOTE: the following regular expression searches for 0xf4 followed // by 0x77. - setValueText("\\xf4\\x77"); - assertEquals("", hexLabel.getText()); + setInput("\\xf4\\x77"); List
addrs = addrs(0x01001042, 0x01001046); - pressSearchAllButton(); - waitForSearch("Search Memory - ", 2); + performSearchAll(); + waitForSearch(2); checkMarkerSet(addrs); } @@ -275,24 +225,21 @@ public class MemSearchRegExTest extends AbstractMemSearchTest { // NOTE: the following regular expression searches for 0x56 followed // by 0-10 occurrences of any character, followed by 0x10. - setValueText("\\x56.{0,10}\\x10"); + setInput("\\x56.{0,10}\\x10"); - assertEquals("", hexLabel.getText()); + setAlignment(4); - setAlignment("4"); - - pressSearchAllButton(); - waitForSearch("Search Memory - ", 1); + performSearchAll(); + waitForSearch(1); checkMarkerSet(addrs(0x01002cfc)); } @Test public void testRegExpHighlight() throws Exception { - setValueText("\\x6a\\x01"); - pressSearchAllButton(); - - waitForSearch("Search Memory - ", 3); + setInput("\\x6a\\x01"); + performSearchAll(); + waitForSearch(3); Highlight[] h = getByteHighlights(addr(0x10029cb), "6a 01"); assertEquals(1, h.length); @@ -302,10 +249,9 @@ public class MemSearchRegExTest extends AbstractMemSearchTest { @Test public void testRegExpHighlight2() throws Exception { - setValueText("\\x6a\\x01"); - pressSearchAllButton(); - - waitForSearch("Search Memory - ", 3); + setInput("\\x6a\\x01"); + performSearchAll(); + waitForSearch(3); Highlight[] h = getByteHighlights(addr(0x1002826), "6a 01"); assertEquals(1, h.length); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchDecimal1Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchScanTest.java similarity index 51% rename from Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchDecimal1Test.java rename to Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchScanTest.java index 3c116e4c58..b1a38149b0 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MemSearchDecimal1Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MemSearchScanTest.java @@ -4,16 +4,16 @@ * 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.plugin.core.searchmem; +package ghidra.features.base.memsearch; import java.nio.charset.StandardCharsets; import java.util.List; @@ -21,25 +21,28 @@ import java.util.List; import org.junit.Before; import org.junit.Test; +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.features.base.memsearch.scan.Scanner; import ghidra.program.database.ProgramBuilder; import ghidra.program.model.address.Address; import ghidra.program.model.data.Pointer32DataType; import ghidra.program.model.listing.Program; /** - * Tests for searching for decimal values in memory. + * Tests for the search results "scan" feature */ -public class MemSearchDecimal1Test extends AbstractMemSearchTest { +public class MemSearchScanTest extends AbstractMemSearchTest { - public MemSearchDecimal1Test() { + public MemSearchScanTest() { super(); } - @Override @Before + @Override public void setUp() throws Exception { super.setUp(); - selectRadioButton("Decimal"); + setSearchFormat(SearchFormat.DECIMAL); + setDecimalSize(4); } @Override @@ -67,7 +70,8 @@ public class MemSearchDecimal1Test extends AbstractMemSearchTest { builder.setBytes("0x1001004", "85 4f dc 77"); builder.applyDataType("0x1001004", new Pointer32DataType(), 1); builder.createEncodedString("0x01001708", "Notepad", StandardCharsets.UTF_16BE, true); - builder.createEncodedString("0x01001740", "something else", StandardCharsets.UTF_16BE, true); + builder.createEncodedString("0x01001740", "something else", StandardCharsets.UTF_16BE, + true); builder.createEncodedString("0x010013cc", "notepad.exe", StandardCharsets.US_ASCII, false); //create some undefined data @@ -84,26 +88,109 @@ public class MemSearchDecimal1Test extends AbstractMemSearchTest { } @Test - public void testSearchByteBackward() throws Exception { + public void testScanEquals() throws Exception { - goTo(0x01002d6d); + setInput("100"); + performSearchAll(); + waitForSearch(3); - selectRadioButton("Byte"); + List
addrs = addrs(0x1001715, 0x1004100, 0x1004135); + checkMarkerSet(addrs); - setValueText("8"); + setValue(addr(0x1004100), 101); - List
addrs = addrs(0x1002d5b, 0x1002d3e); + // only keep values that don't change + scan(Scanner.EQUALS); + waitForSearch(2); - performSearchTest(addrs, "Previous"); + // the address we changed should now be removed from the results + addrs = addrs(0x1001715, 0x1004135); + checkMarkerSet(addrs); } -//================================================================================================== -// Private Methods -//================================================================================================== + @Test + public void testScanNotEquals() throws Exception { - @Override - protected void showMemSearchDialog() { - super.showMemSearchDialog(); - selectRadioButton("Decimal"); + setInput("100"); + performSearchAll(); + waitForSearch(3); + + List
addrs = addrs(0x1001715, 0x1004100, 0x1004135); + checkMarkerSet(addrs); + + setValue(addr(0x1004100), 101); + + // only keep values that don't change + scan(Scanner.NOT_EQUALS); + waitForSearch(1); + + // the address we changed should now be removed from the results + addrs = addrs(0x1004100); + checkMarkerSet(addrs); + } + + @Test + public void testScanIncrement() throws Exception { + + setInput("100"); + performSearchAll(); + waitForSearch(3); + + List
addrs = addrs(0x1001715, 0x1004100, 0x1004135); + checkMarkerSet(addrs); + + setValue(addr(0x1004100), 101); + setValue(addr(0x1004135), 99); + + // only keep values that don't change + scan(Scanner.INCREASED); + waitForSearch(1); + + // the address we changed should now be removed from the results + addrs = addrs(0x1004100); + checkMarkerSet(addrs); + } + + @Test + public void testScanDecrement() throws Exception { + + setInput("100"); + performSearchAll(); + waitForSearch(3); + + List
addrs = addrs(0x1001715, 0x1004100, 0x1004135); + checkMarkerSet(addrs); + + setValue(addr(0x1004100), 101); + setValue(addr(0x1004135), 99); + + // only keep values that don't change + scan(Scanner.DECREASED); + waitForSearch(1); + + // the address we changed should now be removed from the results + addrs = addrs(0x1004135); + checkMarkerSet(addrs); + } + + private void scan(Scanner scanner) { + runSwing(() -> searchProvider.scan(scanner)); + } + + private void setValue(Address address, int value) throws Exception { + byte[] bytes = getBytes(value); + + int transactionID = program.startTransaction("test"); + memory.setBytes(address, bytes); + program.endTransaction(transactionID, true); + } + + private byte[] getBytes(int value) { + byte[] bytes = new byte[4]; + bytes[0] = (byte) (value & 0xff); + bytes[1] = (byte) ((value >> 8) & 0xff); + bytes[2] = (byte) ((value >> 16) & 0xff); + bytes[3] = (byte) ((value >> 24) & 0xff); + return bytes; } } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MnemonicSearchPluginTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MnemonicSearchPluginTest.java similarity index 80% rename from Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MnemonicSearchPluginTest.java rename to Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MnemonicSearchPluginTest.java index 4d39a84f11..aa954ad0a3 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/searchmem/MnemonicSearchPluginTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/memsearch/MnemonicSearchPluginTest.java @@ -4,25 +4,21 @@ * 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.plugin.core.searchmem; +package ghidra.features.base.memsearch; import static org.junit.Assert.*; -import java.awt.Container; import java.awt.Window; -import javax.swing.JComboBox; -import javax.swing.JTextField; - import org.junit.*; import docking.action.DockingActionIf; @@ -30,8 +26,10 @@ import ghidra.app.events.ProgramSelectionPluginEvent; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.app.plugin.core.marker.MarkerManagerPlugin; import ghidra.app.plugin.core.programtree.ProgramTreePlugin; -import ghidra.app.plugin.core.searchmem.mask.MnemonicSearchPlugin; +import ghidra.app.plugin.core.searchmem.MemSearchPlugin; import ghidra.app.services.ProgramManager; +import ghidra.features.base.memsearch.gui.MemorySearchProvider; +import ghidra.features.base.memsearch.mnemonic.MnemonicSearchPlugin; import ghidra.framework.plugintool.PluginTool; import ghidra.program.database.ProgramBuilder; import ghidra.program.database.ProgramDB; @@ -40,6 +38,7 @@ import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramSelection; import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.test.TestEnv; +import ghidra.util.Swing; public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTest { private TestEnv env; @@ -50,6 +49,7 @@ public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTes private DockingActionIf searchMnemonicNoOperandsNoConstAction; private DockingActionIf searchMnemonicOperandsConstAction; private CodeBrowserPlugin cb; + private MemorySearchProvider searchProvider; @Before public void setUp() throws Exception { @@ -97,18 +97,12 @@ public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTes tool.firePluginEvent(new ProgramSelectionPluginEvent("Test", sel, program)); performAction(searchMnemonicOperandsNoConstAction, cb.getProvider(), true); + searchProvider = waitForComponentProvider(MemorySearchProvider.class); - MemSearchDialog dialog = waitForDialogComponent(MemSearchDialog.class); - assertNotNull(dialog); - Container component = dialog.getComponent(); - - @SuppressWarnings("unchecked") - JComboBox comboBox = findComponent(component, JComboBox.class); - JTextField valueField = (JTextField) comboBox.getEditor().getEditorComponent(); - + assertNotNull(searchProvider); assertEquals( "01010101 10001011 11101100 10000001 11101100 ........ ........ ........ ........", - valueField.getText().strip()); + getInput()); } @@ -119,17 +113,12 @@ public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTes performAction(searchMnemonicNoOperandsNoConstAction, cb.getProvider(), true); - MemSearchDialog dialog = waitForDialogComponent(MemSearchDialog.class); - assertNotNull(dialog); - Container component = dialog.getComponent(); - - @SuppressWarnings("unchecked") - JComboBox comboBox = findComponent(component, JComboBox.class); - JTextField valueField = (JTextField) comboBox.getEditor().getEditorComponent(); + searchProvider = waitForComponentProvider(MemorySearchProvider.class); + assertNotNull(searchProvider); assertEquals( "01010... 10001011 11...... 10000001 11101... ........ ........ ........ ........", - valueField.getText().strip()); + getInput()); } @@ -140,17 +129,14 @@ public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTes performAction(searchMnemonicOperandsConstAction, cb.getProvider(), true); - MemSearchDialog dialog = waitForDialogComponent(MemSearchDialog.class); - assertNotNull(dialog); - Container component = dialog.getComponent(); + performAction(searchMnemonicOperandsConstAction, cb.getProvider(), true); - @SuppressWarnings("unchecked") - JComboBox comboBox = findComponent(component, JComboBox.class); - JTextField valueField = (JTextField) comboBox.getEditor().getEditorComponent(); + searchProvider = waitForComponentProvider(MemorySearchProvider.class); + assertNotNull(searchProvider); assertEquals( "01010101 10001011 11101100 10000001 11101100 00000100 00000001 00000000 00000000", - valueField.getText().strip()); + getInput()); } /** @@ -186,4 +172,7 @@ public class MnemonicSearchPluginTest extends AbstractGhidraHeadedIntegrationTes return program.getMinAddress().getNewAddress(offset); } + protected String getInput() { + return Swing.runNow(() -> searchProvider.getSearchInput()); + } } diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/ByteArrayByteSequence.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/ByteArrayByteSequence.java new file mode 100644 index 0000000000..e04bc5af86 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/ByteArrayByteSequence.java @@ -0,0 +1,51 @@ +/* ### + * 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.features.base.memsearch.bytesequence; + +public class ByteArrayByteSequence implements ByteSequence { + + private final byte[] bytes; + + public ByteArrayByteSequence(byte... bytes) { + this.bytes = bytes; + } + + @Override + public int getLength() { + return bytes.length; + } + + @Override + public byte getByte(int i) { + return bytes[i]; + } + + @Override + public byte[] getBytes(int index, int size) { + if (index < 0 || index + size > bytes.length) { + throw new IndexOutOfBoundsException(); + } + byte[] results = new byte[size]; + System.arraycopy(bytes, index, results, 0, size); + return results; + } + + @Override + public boolean hasAvailableBytes(int index, int length) { + return index >= 0 && index + length <= getLength(); + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/ByteArrayByteSequenceTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/ByteArrayByteSequenceTest.java new file mode 100644 index 0000000000..b4385acb52 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/ByteArrayByteSequenceTest.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.features.base.memsearch.bytesequence; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class ByteArrayByteSequenceTest { + private ByteSequence main = new ByteArrayByteSequence((byte) 0, (byte) 1, (byte) 2, (byte) 3); + private ByteSequence extra = new ByteArrayByteSequence((byte) 4, (byte) 5); + private ByteSequence extended = new ExtendedByteSequence(main, extra, 100); + + @Test + public void testSimpleByteSeqeunce() { + assertEquals(4, main.getLength()); + assertEquals(0, main.getByte(0)); + assertEquals(1, main.getByte(1)); + assertEquals(2, main.getByte(2)); + assertEquals(3, main.getByte(3)); + try { + main.getByte(4); + fail("Expected index out of bounds exception"); + } + catch (IndexOutOfBoundsException e) { + // expected + } + } + + @Test + public void testSimpleGetAvailableBytes() { + assertTrue(main.hasAvailableBytes(0, 1)); + assertTrue(main.hasAvailableBytes(0, 2)); + assertTrue(main.hasAvailableBytes(0, 3)); + assertTrue(main.hasAvailableBytes(0, 4)); + assertFalse(main.hasAvailableBytes(0, 5)); + + assertTrue(main.hasAvailableBytes(1, 1)); + assertTrue(main.hasAvailableBytes(1, 2)); + assertTrue(main.hasAvailableBytes(1, 3)); + assertFalse(main.hasAvailableBytes(1, 4)); + assertFalse(main.hasAvailableBytes(1, 5)); + + assertTrue(main.hasAvailableBytes(2, 1)); + assertTrue(main.hasAvailableBytes(2, 2)); + assertFalse(main.hasAvailableBytes(2, 3)); + assertFalse(main.hasAvailableBytes(2, 4)); + + assertTrue(main.hasAvailableBytes(3, 1)); + assertFalse(main.hasAvailableBytes(3, 2)); + assertFalse(main.hasAvailableBytes(3, 3)); + + assertFalse(main.hasAvailableBytes(4, 1)); + assertFalse(main.hasAvailableBytes(4, 2)); + + } + + @Test + public void testExtendedByteSeqeunce() { + assertEquals(4, extended.getLength()); + assertEquals(0, extended.getByte(0)); + assertEquals(1, extended.getByte(1)); + assertEquals(2, extended.getByte(2)); + assertEquals(3, extended.getByte(3)); + assertEquals(4, extended.getByte(4)); + assertEquals(5, extended.getByte(5)); + try { + extended.getByte(6); + fail("Expected index out of bounds exception"); + } + catch (IndexOutOfBoundsException e) { + // expected + } + } + + @Test + public void testExtendedGetAvailableBytes() { + + assertTrue(extended.hasAvailableBytes(0, 1)); + assertTrue(extended.hasAvailableBytes(0, 2)); + assertTrue(extended.hasAvailableBytes(0, 3)); + assertTrue(extended.hasAvailableBytes(0, 4)); + assertTrue(extended.hasAvailableBytes(0, 5)); + assertTrue(extended.hasAvailableBytes(0, 6)); + assertFalse(extended.hasAvailableBytes(0, 7)); + + assertTrue(extended.hasAvailableBytes(1, 1)); + assertTrue(extended.hasAvailableBytes(1, 2)); + assertTrue(extended.hasAvailableBytes(1, 3)); + assertTrue(extended.hasAvailableBytes(1, 4)); + assertTrue(extended.hasAvailableBytes(1, 5)); + assertFalse(extended.hasAvailableBytes(1, 6)); + assertFalse(extended.hasAvailableBytes(1, 7)); + + assertTrue(extended.hasAvailableBytes(2, 1)); + assertTrue(extended.hasAvailableBytes(2, 2)); + assertTrue(extended.hasAvailableBytes(2, 3)); + assertTrue(extended.hasAvailableBytes(2, 4)); + assertFalse(extended.hasAvailableBytes(2, 5)); + assertFalse(extended.hasAvailableBytes(2, 6)); + + assertTrue(extended.hasAvailableBytes(3, 1)); + assertTrue(extended.hasAvailableBytes(3, 2)); + assertTrue(extended.hasAvailableBytes(3, 3)); + assertFalse(extended.hasAvailableBytes(3, 4)); + assertFalse(extended.hasAvailableBytes(3, 5)); + + assertTrue(extended.hasAvailableBytes(4, 1)); + assertTrue(extended.hasAvailableBytes(4, 2)); + assertFalse(extended.hasAvailableBytes(4, 3)); + assertFalse(extended.hasAvailableBytes(4, 4)); + + assertTrue(extended.hasAvailableBytes(5, 1)); + assertFalse(extended.hasAvailableBytes(5, 2)); + assertFalse(extended.hasAvailableBytes(5, 3)); + + assertFalse(extended.hasAvailableBytes(6, 1)); + + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/MaskedBytesSequenceByteMatcherTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/MaskedBytesSequenceByteMatcherTest.java new file mode 100644 index 0000000000..aaec032521 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/MaskedBytesSequenceByteMatcherTest.java @@ -0,0 +1,116 @@ +/* ### + * 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.features.base.memsearch.bytesequence; + +import static org.junit.Assert.*; + +import java.util.Iterator; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; +import ghidra.features.base.memsearch.matcher.ByteMatcher.ByteMatch; +import ghidra.features.base.memsearch.matcher.MaskedByteSequenceByteMatcher; + +public class MaskedBytesSequenceByteMatcherTest { + + private ExtendedByteSequence byteSequence; + + @Before + public void setUp() { + + ByteSequence main = new ByteArrayByteSequence(makeBytes(1, 2, 3, 2, 4, 5, 2, 6, 2, 3, 2)); + ByteSequence extra = new ByteArrayByteSequence(makeBytes(4, 1, 1, 3, 2, 4)); + + byteSequence = new ExtendedByteSequence(main, extra, 100); + + } + + @Test + public void testSimplePatterWithOneMatchCrossingBoundary() { + + byte[] searchBytes = makeBytes(3, 2, 4); + ByteMatcher byteMatcher = new MaskedByteSequenceByteMatcher("", searchBytes, null); + + Iterator it = byteMatcher.match(byteSequence).iterator(); + + assertTrue(it.hasNext()); + assertEquals(new ByteMatch(2, 3), it.next()); + + assertTrue(it.hasNext()); + assertEquals(new ByteMatch(9, 3), it.next()); + + assertFalse(it.hasNext()); + + } + + @Test + public void testSimplePatterWithOneMatchCrossingBoundaryNoHasNextCalls() { + + byte[] searchBytes = makeBytes(3, 2, 4); + ByteMatcher byteMatcher = new MaskedByteSequenceByteMatcher("", searchBytes, null); + + Iterator it = byteMatcher.match(byteSequence).iterator(); + + assertEquals(new ByteMatch(2, 3), it.next()); + assertEquals(new ByteMatch(9, 3), it.next()); + assertNull(it.next()); + } + + @Test + public void testMaskPattern() { + + byte[] searchBytes = makeBytes(2, 0, 2); + byte[] masks = makeBytes(0xff, 0x00, 0xff); + ByteMatcher byteMatcher = + new MaskedByteSequenceByteMatcher("", searchBytes, masks, null); + + Iterator it = byteMatcher.match(byteSequence).iterator(); + + assertEquals(new ByteMatch(1, 3), it.next()); + assertEquals(new ByteMatch(6, 3), it.next()); + assertEquals(new ByteMatch(8, 3), it.next()); + assertNull(it.next()); + } + + @Test + public void testPatternStartButNotEnoughExtraBytes() { + byte[] searchBytes = makeBytes(6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + byte[] masks = makeBytes(0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + ByteMatcher byteMatcher = + new MaskedByteSequenceByteMatcher("", searchBytes, masks, null); + + Iterator it = byteMatcher.match(byteSequence).iterator(); + assertFalse(it.hasNext()); + } + + @Test + public void testGetDescription() { + byte[] searchBytes = makeBytes(1, 2, 3, 0xaa); + ByteMatcher byteMatcher = new MaskedByteSequenceByteMatcher("", searchBytes, null); + + assertEquals("01 02 03 aa", byteMatcher.getDescription()); + } + + private static byte[] makeBytes(int... byteValues) { + byte[] bytes = new byte[byteValues.length]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) byteValues[i]; + } + return bytes; + } +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/RegExByteMatcherTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/RegExByteMatcherTest.java new file mode 100644 index 0000000000..ff027deffc --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/RegExByteMatcherTest.java @@ -0,0 +1,86 @@ +/* ### + * 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.features.base.memsearch.bytesequence; + +import static org.junit.Assert.*; + +import java.util.Iterator; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; +import ghidra.features.base.memsearch.matcher.ByteMatcher.ByteMatch; +import ghidra.features.base.memsearch.matcher.RegExByteMatcher; + +public class RegExByteMatcherTest { + private ExtendedByteSequence byteSequence; + + @Before + public void setUp() { + ByteSequence main = new ByteArrayByteSequence(makeBytes("one two three tw")); + ByteSequence extra = new ByteArrayByteSequence(makeBytes("o four two five")); + byteSequence = new ExtendedByteSequence(main, extra, 100); + + } + + @Test + public void testSimplePatternWithOneMatchCrossingBoundary() { + + ByteMatcher byteMatcher = new RegExByteMatcher("two", null); + + Iterator it = byteMatcher.match(byteSequence).iterator(); + + assertTrue(it.hasNext()); + assertEquals(new ByteMatch(4, 3), it.next()); + + assertTrue(it.hasNext()); + assertEquals(new ByteMatch(14, 3), it.next()); + + assertFalse(it.hasNext()); + + } + + @Test + public void testSimplePatternWithOneMatchCrossingBoundaryNoHasNextCalls() { + + ByteMatcher byteMatcher = new RegExByteMatcher("two", null); + + Iterator it = byteMatcher.match(byteSequence).iterator(); + + assertEquals(new ByteMatch(4, 3), it.next()); + assertEquals(new ByteMatch(14, 3), it.next()); + assertNull(it.next()); + } + + @Test + public void testNoMatch() { + + ByteMatcher byteMatcher = new RegExByteMatcher("apple", null); + + Iterator it = byteMatcher.match(byteSequence).iterator(); + assertFalse(it.hasNext()); + } + + private byte[] makeBytes(String string) { + byte[] bytes = new byte[string.length()]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) string.charAt(i); + } + return bytes; + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/combiner/MemoryMatchCombinerTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/combiner/MemoryMatchCombinerTest.java new file mode 100644 index 0000000000..7c20715e4b --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/combiner/MemoryMatchCombinerTest.java @@ -0,0 +1,345 @@ +/* ### + * 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.features.base.memsearch.combiner; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.features.base.memsearch.searcher.MemoryMatch; +import ghidra.program.model.address.*; + +public class MemoryMatchCombinerTest { + + private GenericAddressSpace space; + + private MemoryMatch m1; + private MemoryMatch m2; + private MemoryMatch m3; + private MemoryMatch m4; + private MemoryMatch m5; + private MemoryMatch m6; + private MemoryMatch m7; + private MemoryMatch m8; + + List list1; + List list2; + List result; + + @Before + public void setUp() { + space = new GenericAddressSpace("test", 64, AddressSpace.TYPE_RAM, 0); + + m1 = createMatch(1, 4); + m2 = createMatch(2, 4); + m3 = createMatch(3, 4); + m4 = createMatch(4, 4); + m5 = createMatch(5, 4); + m6 = createMatch(6, 4); + m7 = createMatch(7, 4); + m8 = createMatch(8, 4); + } + + @Test + public void testUnionAllUnique() { + list1 = list(m1, m8); + list2 = list(m2, m3, m4); + result = union(list1, list2); + assertEquals(list(m1, m2, m3, m4, m8), result); + } + + @Test + public void testUnionWithEmptyList() { + list1 = list(m1, m8); + list2 = list(); + result = union(list1, list2); + assertEquals(list(m1, m8), result); + + list1 = list(); + list2 = list(m5, m7); + result = union(list1, list2); + assertEquals(list(m5, m7), result); + + } + + @Test + public void testUnionWithDups() { + list1 = list(m1, m2, m3); + list2 = list(m3, m4, m5); + result = union(list1, list2); + assertEquals(list(m1, m2, m3, m4, m5), result); + } + + @Test + public void testUnionWithDupsKeepsLonger() { + MemoryMatch m3_short = createMatch(3, 2); + MemoryMatch m3_long = createMatch(3, 8); + + list1 = list(m1, m2, m3); + list2 = list(m3_short, m4, m5); + result = union(list1, list2); + assertEquals(list(m1, m2, m3, m4, m5), result); + + list2 = list(m3_long, m4, m5); + result = union(list1, list2); + assertEquals(list(m1, m2, m3_long, m4, m5), result); + } + + @Test + public void testIntersectionAllUnique() { + list1 = list(m1, m8); + list2 = list(m2, m3, m4); + result = intersect(list1, list2); + assertEquals(list(), result); + } + + @Test + public void testIntersectionAllSame() { + list1 = list(m1, m2); + list2 = list(m1, m2); + result = intersect(list1, list2); + assertEquals(list(m1, m2), result); + } + + @Test + public void testIntersectionSomeSameSomeUnique() { + list1 = list(m1, m2, m3); + list2 = list(m2, m3, m4); + result = intersect(list1, list2); + assertEquals(list(m2, m3), result); + } + + @Test + public void testIntersectionKeepsLonger() { + MemoryMatch m4_long = createMatch(4, 8); + MemoryMatch m3_long = createMatch(3, 8); + list1 = list(m1, m2, m3, m4_long); + list2 = list(m1, m2, m3_long, m4); + result = intersect(list1, list2); + assertEquals(list(m1, m2, m3_long, m4_long), result); + } + + @Test + public void testXor() { + list1 = list(m1, m2, m3, m4); + list2 = list(m3, m4, m5, m6); + result = xor(list1, list2); + assertEquals(list(m1, m2, m5, m6), result); + } + + @Test + public void testXorNothingInCommon() { + list1 = list(m1, m2); + list2 = list(m3, m4); + result = xor(list1, list2); + assertEquals(list(m1, m2, m3, m4), result); + } + + @Test + public void testXorAllInCommon() { + list1 = list(m1, m2); + list2 = list(m1, m2); + result = xor(list1, list2); + assertEquals(list(), result); + } + + @Test + public void testXorWithEmpty() { + list1 = list(m1, m2); + list2 = list(); + result = xor(list1, list2); + assertEquals(list(m1, m2), result); + + list1 = list(); + list2 = list(m1, m2); + result = xor(list1, list2); + assertEquals(list(m1, m2), result); + } + + @Test + public void testXorLengthDontMatter() { + MemoryMatch m4_long = createMatch(4, 8); + MemoryMatch m3_short = createMatch(3, 2); + + list1 = list(m1, m2, m3, m4); + list2 = list(m3_short, m4_long, m5); + result = xor(list1, list2); + assertEquals(list(m1, m2, m5), result); + + list1 = list(m1, m2, m3_short, m4_long); + list2 = list(m3, m4, m5); + result = xor(list1, list2); + assertEquals(list(m1, m2, m5), result); + } + + @Test + public void testAMinusB() { + list1 = list(m1, m2, m3, m4); + list2 = list(m2, m3); + result = aMinusB(list1, list2); + assertEquals(list(m1, m4), result); + } + + @Test + public void testAMinusBSameSet() { + list1 = list(m1, m2, m3, m4); + list2 = list(m1, m2, m3, m4); + result = aMinusB(list1, list2); + assertEquals(list(), result); + } + + @Test + public void testAMinusBNothingInCommon() { + list1 = list(m1, m2, m3, m4); + list2 = list(m5, m6, m7, m8); + result = aMinusB(list1, list2); + assertEquals(list(m1, m2, m3, m4), result); + } + + @Test + public void testAMinusBEmptyList() { + list1 = list(); + list2 = list(m5, m6, m7, m8); + result = aMinusB(list1, list2); + assertEquals(list(), result); + + list1 = list(m5, m6, m7, m8); + list2 = list(); + result = aMinusB(list1, list2); + assertEquals(list(m5, m6, m7, m8), result); + } + + @Test + public void testAMinusBLengthDontMatter() { + MemoryMatch m4_long = createMatch(4, 8); + MemoryMatch m3_short = createMatch(3, 2); + + list1 = list(m1, m2, m3, m4); + list2 = list(m3_short, m4_long, m5); + result = aMinusB(list1, list2); + assertEquals(list(m1, m2), result); + + list1 = list(m1, m2, m3_short, m4_long); + list2 = list(m3, m4, m5); + result = aMinusB(list1, list2); + assertEquals(list(m1, m2), result); + } + + @Test + public void testBMinusA() { + list1 = list(m1, m2, m3, m4); + list2 = list(m2, m3, m4, m5, m6); + result = BMinusA(list1, list2); + assertEquals(list(m5, m6), result); + } + + @Test + public void testBMinusASameSet() { + list1 = list(m1, m2, m3, m4); + list2 = list(m1, m2, m3, m4); + result = BMinusA(list1, list2); + assertEquals(list(), result); + } + + @Test + public void testBMinusANothingInCommon() { + list1 = list(m1, m2, m3, m4); + list2 = list(m5, m6, m7, m8); + result = BMinusA(list1, list2); + assertEquals(list(m5, m6, m7, m8), result); + } + + @Test + public void testBMinusAEmptyList() { + list1 = list(); + list2 = list(m5, m6, m7, m8); + result = BMinusA(list1, list2); + assertEquals(list(m5, m6, m7, m8), result); + + list1 = list(m5, m6, m7, m8); + list2 = list(); + result = BMinusA(list1, list2); + assertEquals(list(), result); + } + + @Test + public void testBMinusALengthDontMatter() { + MemoryMatch m4_long = createMatch(4, 8); + MemoryMatch m3_short = createMatch(3, 2); + + list1 = list(m1, m2, m3, m4); + list2 = list(m3_short, m4_long, m5); + result = BMinusA(list1, list2); + assertEquals(list(m5), result); + + list1 = list(m1, m2, m3_short, m4_long); + list2 = list(m3, m4, m5); + result = BMinusA(list1, list2); + assertEquals(list(m5), result); + } + + private List xor(List matches1, List matches2) { + Combiner combiner = Combiner.XOR; + List results = new ArrayList<>(combiner.combine(matches1, matches2)); + Collections.sort(results); + return results; + } + + private List union(List matches1, List matches2) { + Combiner combiner = Combiner.UNION; + List results = new ArrayList<>(combiner.combine(matches1, matches2)); + Collections.sort(results); + return results; + } + + private List intersect(List matches1, List matches2) { + Combiner combiner = Combiner.INTERSECT; + List results = new ArrayList<>(combiner.combine(matches1, matches2)); + Collections.sort(results); + return results; + } + + private List aMinusB(List matches1, List matches2) { + Combiner combiner = Combiner.A_MINUS_B; + List results = new ArrayList<>(combiner.combine(matches1, matches2)); + Collections.sort(results); + return results; + } + + private List BMinusA(List matches1, List matches2) { + Combiner combiner = Combiner.B_MINUS_A; + List results = new ArrayList<>(combiner.combine(matches1, matches2)); + Collections.sort(results); + return results; + } + + private List list(MemoryMatch... matches) { + return Arrays.asList(matches); + } + + private Address addr(long offset) { + return space.getAddress(offset); + } + + private MemoryMatch createMatch(int offset, int length) { + Address address = addr(offset); + byte[] bytes = new byte[length]; + return new MemoryMatch(address, bytes, null); + } +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/AbstractSearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/AbstractSearchFormatTest.java new file mode 100644 index 0000000000..7a40692afe --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/AbstractSearchFormatTest.java @@ -0,0 +1,98 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import ghidra.features.base.memsearch.gui.SearchSettings; +import ghidra.features.base.memsearch.matcher.ByteMatcher; +import ghidra.features.base.memsearch.matcher.MaskedByteSequenceByteMatcher; + +public class AbstractSearchFormatTest { + protected SearchFormat format; + protected MaskedByteSequenceByteMatcher matcher; + protected SearchSettings settings = new SearchSettings().withBigEndian(true); + + protected SearchSettings hexSettings = settings.withSearchFormat(SearchFormat.HEX); + protected SearchSettings binarySettings = settings.withSearchFormat(SearchFormat.BINARY); + protected SearchSettings decimalSettings = settings.withSearchFormat(SearchFormat.DECIMAL); + protected SearchSettings int1Settings = decimalSettings.withDecimalByteSize(1); + protected SearchSettings int2Settings = decimalSettings.withDecimalByteSize(2); + protected SearchSettings int4Settings = decimalSettings.withDecimalByteSize(4); + protected SearchSettings int8Settings = decimalSettings.withDecimalByteSize(8); + protected SearchSettings uint1Settings = int1Settings.withDecimalUnsigned(true); + protected SearchSettings uint2Settings = int2Settings.withDecimalUnsigned(true); + protected SearchSettings uint4Settings = int4Settings.withDecimalUnsigned(true); + protected SearchSettings uint8Settings = int8Settings.withDecimalUnsigned(true); + protected SearchSettings floatSettings = settings.withSearchFormat(SearchFormat.FLOAT); + protected SearchSettings doubleSettings = settings.withSearchFormat(SearchFormat.DOUBLE); + protected SearchSettings stringSettings = settings.withSearchFormat(SearchFormat.STRING); + protected SearchSettings regExSettings = settings.withSearchFormat(SearchFormat.REG_EX); + + protected AbstractSearchFormatTest(SearchFormat format) { + this.format = format; + this.settings = settings.withSearchFormat(format); + } + + protected MaskedByteSequenceByteMatcher parse(String string) { + ByteMatcher byteMatcher = format.parse(string, settings); + if (byteMatcher instanceof MaskedByteSequenceByteMatcher m) { + return m; + } + fail("Expected MaskedByteSequenceByteMatcher, but got " + byteMatcher); + return null; + } + + protected static byte[] bytes(int... byteValues) { + byte[] bytes = new byte[byteValues.length]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) byteValues[i]; + } + return bytes; + } + + protected void assertBytes(int... expectedValues) { + byte[] bytes = matcher.getBytes(); + byte[] expectedBytes = bytes(expectedValues); + assertArrayEquals(expectedBytes, bytes); + } + + protected void assertMask(int... expectedValues) { + byte[] bytes = matcher.getMask(); + byte[] expectedBytes = bytes(expectedValues); + assertArrayEquals(expectedBytes, bytes); + } + + protected int compareBytes(String input1, String input2) { + byte[] bytes1 = getBytes(input1); + byte[] bytes2 = getBytes(input2); + return format.compareValues(bytes1, bytes2, settings); + } + + protected byte[] getBytes(String input) { + matcher = parse(input); + return matcher.getBytes(); + } + + protected String str(long value) { + return Long.toString(value); + } + + protected String convertText(SearchSettings oldSettings, String text) { + return format.convertText(text, oldSettings, settings); + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/BinarySearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/BinarySearchFormatTest.java new file mode 100644 index 0000000000..5650259a7d --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/BinarySearchFormatTest.java @@ -0,0 +1,115 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class BinarySearchFormatTest extends AbstractSearchFormatTest { + public BinarySearchFormatTest() { + super(SearchFormat.BINARY); + } + + @Test + public void testSimpleCase() { + matcher = parse("0 1 10001000 11111111"); + + assertBytes(0, 1, 0x88, 0xff); + assertMask(0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testWildCards() { + matcher = parse("111.111?"); + assertBytes(0xee); + assertMask(0xee); + + matcher = parse("....0001"); + assertBytes(0x01); + assertMask(0x0f); + } + + @Test + public void testGroupTooBig() { + ByteMatcher bad = format.parse("111111111", settings); + assertFalse(bad.isValidInput()); + assertEquals("Max group size exceeded. Enter to add more.", bad.getDescription()); + } + + @Test + public void testInvalidChars() { + ByteMatcher bad = format.parse("012", settings); + assertFalse(bad.isValidInput()); + assertEquals("Invalid character", bad.getDescription()); + } + + @Test + public void testConvertTextFromHex() { + assertEquals("10001000 11111111", convertText(hexSettings, "88 ff")); + assertEquals("00000001 00000000", convertText(hexSettings, "1 0")); + } + + @Test + public void testConvertTextFromSignedInts() { + + assertEquals("00010110 11111111 00000000", convertText(int1Settings, "22 -1 0")); + assertEquals("00000000 00000001", convertText(int2Settings, "1")); + assertEquals("11111111 11111111", convertText(int2Settings, "-1")); + assertEquals("00000000 00000000 00000000 00000001", convertText(int4Settings, "1")); + assertEquals("11111111 11111111 11111111 11111111", convertText(int4Settings, "-1")); + assertEquals("00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001", + convertText(int8Settings, "1")); + assertEquals("11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111", + convertText(int8Settings, "-1")); + } + + @Test + public void testConvertTextFromUnsignedInts() { + + assertEquals("00010110 11111111 00000000", convertText(uint1Settings, "22 255 0")); + assertEquals("00000000 00000001", convertText(uint2Settings, "1")); + assertEquals("11111111 11111111", convertText(uint2Settings, "65535")); + assertEquals("00000000 00000000 00000000 00000001", convertText(uint4Settings, "1")); + assertEquals("11111111 11111111 11111111 11111111", + convertText(uint4Settings, "4294967295")); + assertEquals("00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001", + convertText(uint8Settings, "1")); + assertEquals("11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111", + convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromFloats() { + assertEquals("00111111 10011101 11110011 10110110", + convertText(floatSettings, "1.234")); + assertEquals("00111111 11110011 10111110 01110110 11001000 10110100 00111001 01011000", + convertText(doubleSettings, "1.234")); + } + + @Test + public void testConvertTextFromString() { + assertEquals("", convertText(stringSettings, "Hey")); + assertEquals("", convertText(stringSettings, "56 ab")); + assertEquals("0001", convertText(stringSettings, "0001")); + assertEquals("", convertText(regExSettings, "B*B")); + assertEquals("", convertText(regExSettings, "56 ab")); + assertEquals("0001", convertText(regExSettings, "0001")); + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/DoubleSearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/DoubleSearchFormatTest.java new file mode 100644 index 0000000000..8855a6dca1 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/DoubleSearchFormatTest.java @@ -0,0 +1,204 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class DoubleSearchFormatTest extends AbstractSearchFormatTest { + public DoubleSearchFormatTest() { + super(SearchFormat.DOUBLE); + settings = settings.withBigEndian(true); + } + + @Test + public void testSimpleCaseBigEndian() { + matcher = parse("1.2 1.2"); + assertBytes(63, -13, 51, 51, 51, 51, 51, 51, 63, -13, 51, 51, 51, 51, 51, 51); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff); + } + + @Test + public void testSimpleCaseLittleEndian() { + settings = settings.withBigEndian(false); + matcher = parse("1.2 1.2"); + assertBytes(51, 51, 51, 51, 51, 51, -13, 63, 51, 51, 51, 51, 51, 51, -13, 63); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff); + } + + @Test + public void testECase() { + matcher = parse("1.2e10"); + assertBytes(66, 6, 90, 11, -64, 0, 0, 0); + } + + @Test + public void testNegativeECase() { + matcher = parse("1.2e-10"); + assertBytes(61, -32, 126, 31, -23, 27, 11, 112); + } + + @Test + public void testDotOnly() { + ByteMatcher byteMatcher = format.parse(".", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete floating point number", byteMatcher.getDescription()); + } + + @Test + public void testEndE() { + ByteMatcher byteMatcher = format.parse("2.1e", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete floating point number", byteMatcher.getDescription()); + } + + @Test + public void testEndNegativeE() { + ByteMatcher byteMatcher = format.parse("2.1-e", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete floating point number", byteMatcher.getDescription()); + } + + @Test + public void testNegativeSignOnly() { + ByteMatcher byteMatcher = format.parse("-", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete negative floating point number", byteMatcher.getDescription()); + } + + @Test + public void testNegativeDotSignOnly() { + ByteMatcher byteMatcher = format.parse("-.", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete negative floating point number", byteMatcher.getDescription()); + } + + @Test + public void testBadChars() { + ByteMatcher byteMatcher = format.parse("12.z", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Floating point parse error: For input string: \"12.z\"", + byteMatcher.getDescription()); + } + + @Test + public void testGetValueBigEndian() { + settings = settings.withBigEndian(true); + assertRoundTrip(-1.1234, "-1.1234"); + assertRoundTrip(1.1234, "1.1234"); + assertRoundTrip(0.0, "0.0"); + } + + @Test + public void testGetValueLittleEndian() { + settings = settings.withBigEndian(false); + assertRoundTrip(-1.1234, "-1.1234"); + assertRoundTrip(1.1234, "1.1234"); + assertRoundTrip(0.0, "0.0"); + } + + @Test + public void testCompareValuesBigEndian() { + settings = settings.withBigEndian(true); + assertEquals(0, compareBytes("10.0", "10.0")); + assertEquals(-1, compareBytes("9.0", "10.0")); + assertEquals(1, compareBytes("11.0", "10.0")); + + assertEquals(0, compareBytes("-10.1", "-10.1")); + assertEquals(1, compareBytes("-9.1", "-10.1")); + assertEquals(-1, compareBytes("-11.1", "-10.1")); + + } + + @Test + public void testCompareValuesLittleEndian() { + settings = settings.withBigEndian(false); + + assertEquals(0, compareBytes("10.0", "10.0")); + assertEquals(-1, compareBytes("9.0", "10.0")); + assertEquals(1, compareBytes("11.0", "10.0")); + + assertEquals(0, compareBytes("-10.1", "-10.1")); + assertEquals(1, compareBytes("-9.1", "-10.1")); + assertEquals(-1, compareBytes("-11.1", "-10.1")); + } + + private void assertRoundTrip(double expected, String input) { + matcher = parse(input); + byte[] bytes = matcher.getBytes(); + double value = getFloatFormat().getValue(bytes, 0, settings.isBigEndian()); + assertEquals(expected, value, 0.0000001); + } + + private FloatSearchFormat getFloatFormat() { + return (FloatSearchFormat) format; + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("10001000 11111111", convertText(binarySettings, "10001000 11111111")); + assertEquals("1 0", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromHex() { + assertEquals("56 12", convertText(hexSettings, "56 12")); + assertEquals("1 0", convertText(hexSettings, "1 0")); + } + + @Test + public void testConvertTextFromSignedInts() { + assertEquals("0 22 -1 -14", convertText(int1Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int2Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int4Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int8Settings, "0 22 -1 -14")); + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("0 22 255", convertText(uint1Settings, "0 22 255")); + assertEquals("0 22 65535", convertText(uint2Settings, "0 22 65535")); + assertEquals("22 4294967295", convertText(uint4Settings, "22 4294967295")); + assertEquals("22", convertText(uint8Settings, "22")); + assertEquals("18446744073709551615", + convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromFloats() { + assertEquals("1.234", convertText(floatSettings, "1.234")); + } + + @Test + public void testConvertTextFromString() { + assertEquals("", convertText(stringSettings, "Hey")); + assertEquals("1.23", convertText(stringSettings, "1.23")); + assertEquals("", convertText(regExSettings, "B*B")); + assertEquals("1.23", convertText(regExSettings, "1.23")); + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/FloatSearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/FloatSearchFormatTest.java new file mode 100644 index 0000000000..70d8c7bc2b --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/FloatSearchFormatTest.java @@ -0,0 +1,201 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class FloatSearchFormatTest extends AbstractSearchFormatTest { + public FloatSearchFormatTest() { + super(SearchFormat.FLOAT); + settings = settings.withBigEndian(true); + } + + @Test + public void testSimpleCaseBigEndian() { + matcher = parse("1.2 1.2"); + assertBytes(63, -103, -103, -102, 63, -103, -103, -102); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testSimpleCaseLittleEndian() { + settings = settings.withBigEndian(false); + matcher = parse("1.2 1.2"); + assertBytes(-102, -103, -103, 63, -102, -103, -103, 63); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testECase() { + matcher = parse("1.2e10"); + assertBytes(80, 50, -48, 94); + } + + @Test + public void testNegativeECase() { + matcher = parse("1.2e-10"); + assertBytes(47, 3, -16, -1); + } + + @Test + public void testDotOnly() { + ByteMatcher byteMatcher = format.parse(".", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete floating point number", byteMatcher.getDescription()); + } + + @Test + public void testEndE() { + ByteMatcher byteMatcher = format.parse("2.1e", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete floating point number", byteMatcher.getDescription()); + } + + @Test + public void testEndNegativeE() { + ByteMatcher byteMatcher = format.parse("2.1-e", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete floating point number", byteMatcher.getDescription()); + } + + @Test + public void testNegativeSignOnly() { + ByteMatcher byteMatcher = format.parse("-", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete negative floating point number", byteMatcher.getDescription()); + } + + @Test + public void testNegativeDotSignOnly() { + ByteMatcher byteMatcher = format.parse("-.", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete negative floating point number", byteMatcher.getDescription()); + } + + @Test + public void testBadChars() { + ByteMatcher byteMatcher = format.parse("12.z", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Floating point parse error: For input string: \"12.z\"", + byteMatcher.getDescription()); + } + + @Test + public void testGetValueBigEndian() { + settings = settings.withBigEndian(true); + assertRoundTrip(-1.1234, "-1.1234"); + assertRoundTrip(1.1234, "1.1234"); + assertRoundTrip(0.0, "0.0"); + } + + @Test + public void testGetValueLittleEndian() { + settings = settings.withBigEndian(false); + assertRoundTrip(-1.1234, "-1.1234"); + assertRoundTrip(1.1234, "1.1234"); + assertRoundTrip(0.0, "0.0"); + } + + @Test + public void testCompareValuesBigEndian() { + settings = settings.withBigEndian(true); + assertEquals(0, compareBytes("10.0", "10.0")); + assertEquals(-1, compareBytes("9.0", "10.0")); + assertEquals(1, compareBytes("11.0", "10.0")); + + assertEquals(0, compareBytes("-10.1", "-10.1")); + assertEquals(1, compareBytes("-9.1", "-10.1")); + assertEquals(-1, compareBytes("-11.1", "-10.1")); + + } + + @Test + public void testCompareValuesLittleEndian() { + settings = settings.withBigEndian(false); + + assertEquals(0, compareBytes("10.0", "10.0")); + assertEquals(-1, compareBytes("9.0", "10.0")); + assertEquals(1, compareBytes("11.0", "10.0")); + + assertEquals(0, compareBytes("-10.1", "-10.1")); + assertEquals(1, compareBytes("-9.1", "-10.1")); + assertEquals(-1, compareBytes("-11.1", "-10.1")); + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("10001000 11111111", convertText(binarySettings, "10001000 11111111")); + assertEquals("1 0", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromHex() { + assertEquals("56 12", convertText(hexSettings, "56 12")); + assertEquals("1 0", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromSignedInts() { + assertEquals("0 22 -1 -14", convertText(int1Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int2Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int4Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int8Settings, "0 22 -1 -14")); + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("0 22 255", convertText(uint1Settings, "0 22 255")); + assertEquals("0 22 65535", convertText(uint2Settings, "0 22 65535")); + assertEquals("22 4294967295", convertText(uint4Settings, "22 4294967295")); + assertEquals("22", convertText(uint8Settings, "22")); + assertEquals("18446744073709551615", convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromDoubles() { + assertEquals("1.234", convertText(doubleSettings, "1.234")); + } + + @Test + public void testConvertTextFromString() { + assertEquals("", convertText(stringSettings, "Hey")); + assertEquals("1.23", convertText(stringSettings, "1.23")); + assertEquals("", convertText(regExSettings, "B*B")); + assertEquals("1.23", convertText(regExSettings, "1.23")); + } + + private void assertRoundTrip(double expected, String input) { + matcher = parse(input); + byte[] bytes = matcher.getBytes(); + double value = getFloatFormat().getValue(bytes, 0, settings.isBigEndian()); + assertEquals(expected, value, 0.0000001); + } + + private FloatSearchFormat getFloatFormat() { + return (FloatSearchFormat) format; + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/HexSearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/HexSearchFormatTest.java new file mode 100644 index 0000000000..544ddef6cc --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/HexSearchFormatTest.java @@ -0,0 +1,200 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class HexSearchFormatTest extends AbstractSearchFormatTest { + + public HexSearchFormatTest() { + super(SearchFormat.HEX); + } + + @Test + public void testSimpleLowerCase() { + matcher = parse("1 02 3 aa"); + + assertBytes(1, 2, 3, 0xaa); + assertMask(0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testSimpleUpperCase() { + matcher = parse("1 02 3 AA"); + + assertBytes(1, 2, 3, 0xaa); + assertMask(0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testSimpleWildCardOneDigit() { + matcher = parse("1 . 3"); + assertBytes(1, 0, 3); + assertMask(0xff, 0, 0xff); + + matcher = parse("1 ? 3"); + assertBytes(1, 0, 3); + assertMask(0xff, 0, 0xff); + } + + @Test + public void testSimpleWildCardTwoDigit() { + matcher = parse("1 .. 3"); + assertBytes(1, 0, 3); + assertMask(0xff, 0, 0xff); + + matcher = parse("1 ?? 3"); + assertBytes(1, 0, 3); + assertMask(0xff, 0, 0xff); + + matcher = parse("1 ?. 3"); + assertBytes(1, 0, 3); + assertMask(0xff, 0, 0xff); + } + + @Test + public void testGroupBigEndian() { + settings = settings.withBigEndian(true); + + matcher = parse("1234"); + assertBytes(0x12, 0x34); + assertMask(0xff, 0xff); + + matcher = parse("12345678"); + assertBytes(0x12, 0x34, 0x56, 0x78); + assertMask(0xff, 0xff, 0xff, 0xff); + + matcher = parse("123456789abcdef0"); + assertBytes(0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testOddGroupSizeBigEndian() { + settings = settings.withBigEndian(true); + + matcher = parse("123456"); + assertBytes(0x12, 0x34, 0x56); + assertMask(0xff, 0xff, 0xff); + + matcher = parse("123456789abcde"); + assertBytes(0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testGroupLittleEndian() { + settings = settings.withBigEndian(false); + + matcher = parse("1234"); + assertBytes(0x34, 0x12); + assertMask(0xff, 0xff); + + matcher = parse("12345678"); + assertBytes(0x78, 0x56, 0x34, 0x12); + assertMask(0xff, 0xff, 0xff, 0xff); + + matcher = parse("123456789abcdef0"); + assertBytes(0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testOddGroupSizeLittleEndian() { + settings = settings.withBigEndian(false); + + matcher = parse("123456"); + assertBytes(0x56, 0x34, 0x12); + assertMask(0xff, 0xff, 0xff); + + matcher = parse("123456789abcde"); + assertBytes(0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testGroupsWithWildsBigEndian() { + settings = settings.withBigEndian(true); + matcher = parse("12.45."); + assertBytes(0x12, 0x04, 0x50); + assertMask(0xff, 0x0f, 0xf0); + } + + @Test + public void testGroupsWithWildsLittleEndian() { + settings = settings.withBigEndian(false); + matcher = parse("12.45."); + assertBytes(0x50, 0x04, 0x12); + assertMask(0xf0, 0x0f, 0xff); + } + + @Test + public void testGroupTooBig() { + ByteMatcher bad = format.parse("0123456789abcdef0", settings); + assertFalse(bad.isValidInput()); + assertEquals("Max group size exceeded. Enter to add more.", bad.getDescription()); + } + + @Test + public void testInvalidChars() { + ByteMatcher bad = format.parse("01z3", settings); + assertFalse(bad.isValidInput()); + assertEquals("Invalid character", bad.getDescription()); + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("88 ff", convertText(binarySettings, "10001000 11111111")); + assertEquals("01 00", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromSignedInts() { + + assertEquals("00 16 ff f2", convertText(int1Settings, "0 22 -1 -14")); + assertEquals("00 00 00 16 ff ff ff f2", convertText(int2Settings, "0 22 -1 -14")); + assertEquals("00 00 00 16 ff ff ff f2", convertText(int4Settings, "22 -14")); + assertEquals("00 00 00 00 00 00 00 16", convertText(int8Settings, "22")); + assertEquals("ff ff ff ff ff ff ff f2", convertText(int8Settings, "-14")); + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("00 16 ff", convertText(uint1Settings, "0 22 255")); + assertEquals("00 00 00 16 ff ff", convertText(uint2Settings, "0 22 65535")); + assertEquals("00 00 00 16 ff ff ff ff", convertText(uint4Settings, "22 4294967295")); + assertEquals("00 00 00 00 00 00 00 16", convertText(uint8Settings, "22")); + assertEquals("ff ff ff ff ff ff ff ff", convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromFloats() { + assertEquals("3f 9d f3 b6", convertText(floatSettings, "1.234")); + assertEquals("3f f3 be 76 c8 b4 39 58", convertText(doubleSettings, "1.234")); + } + + @Test + public void testConvertTextFromString() { + assertEquals("", convertText(stringSettings, "Hey")); + assertEquals("56 ab", convertText(stringSettings, "56 ab")); + assertEquals("", convertText(regExSettings, "B*B")); + assertEquals("56 ab", convertText(regExSettings, "56 ab")); + } +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/Int1SearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/Int1SearchFormatTest.java new file mode 100644 index 0000000000..c47c287c2f --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/Int1SearchFormatTest.java @@ -0,0 +1,191 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class Int1SearchFormatTest extends AbstractSearchFormatTest { + public Int1SearchFormatTest() { + super(SearchFormat.DECIMAL); + settings = settings.withDecimalByteSize(1); + } + + @Test + public void testSimpleCaseBigEndian() { + matcher = parse("1 2 -1"); + assertBytes(1, 2, -1); + assertMask(0xff, 0xff, 0xff); + } + + @Test + public void testSimpleCaseLittleEndian() { + settings = settings.withBigEndian(false); + matcher = parse("1 2 -1"); + assertBytes(1, 2, -1); + assertMask(0xff, 0xff, 0xff); + } + + @Test + public void testMinimum() { + long value = Byte.MIN_VALUE; + matcher = parse(Long.toString(value)); + assertBytes(0x80); + + value -= 1; + ByteMatcher byteMatcher = format.parse(Long.toString(value), settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [-128, 127]", + byteMatcher.getDescription()); + } + + @Test + public void testMaximum() { + long value = Byte.MAX_VALUE; + matcher = parse(Long.toString(value)); + assertBytes(0x7f); + + value += 1; + ByteMatcher byteMatcher = format.parse(Long.toString(value), settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [-128, 127]", + byteMatcher.getDescription()); + } + + @Test + public void testNegativeSignOnly() { + ByteMatcher byteMatcher = format.parse("-", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete negative number", byteMatcher.getDescription()); + } + + @Test + public void testBadChars() { + ByteMatcher byteMatcher = format.parse("12z", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Number parse error: For input string: \"12z\"", byteMatcher.getDescription()); + } + + @Test + public void testGetValueBigEndian() { + settings = settings.withBigEndian(true); + assertRoundTrip(-1, "-1"); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(Byte.MAX_VALUE, Integer.toString(Byte.MAX_VALUE)); + assertRoundTrip(Byte.MIN_VALUE, Integer.toString(Byte.MIN_VALUE)); + } + + @Test + public void testGetValueLittleEndian() { + settings = settings.withBigEndian(false); + assertRoundTrip(-1, "-1"); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(Byte.MAX_VALUE, Integer.toString(Byte.MAX_VALUE)); + assertRoundTrip(Byte.MIN_VALUE, Integer.toString(Byte.MIN_VALUE)); + } + + @Test + public void testCompareValuesBigEndian() { + settings = settings.withBigEndian(true); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes("-10", "-10")); + assertEquals(1, compareBytes("-9", "-10")); + assertEquals(-1, compareBytes("-11", "-10")); + + } + + @Test + public void testCompareValuesLittleEndian() { + settings = settings.withBigEndian(false); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes("-10", "-10")); + assertEquals(1, compareBytes("-9", "-10")); + assertEquals(-1, compareBytes("-11", "-10")); + + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("-120 -1", convertText(binarySettings, "10001000 11111111")); + assertEquals("1 0", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromhexSettings() { + assertEquals("86 18", convertText(hexSettings, "56 12")); + assertEquals("1 0", convertText(hexSettings, "1 0")); + } + + @Test + public void testConvertTextFromOtherSizedInts() { + assertEquals("0 22 -1 -14", convertText(int2Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int4Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int8Settings, "0 22 -1 -14")); + + assertEquals("78 32", convertText(int2Settings, "20000")); + assertEquals("1 49 45 0", convertText(int4Settings, "20000000")); + assertEquals("0 0 1 -47 -87 74 32 0", convertText(int8Settings, "2000000000000")); + + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("0 22 -1", convertText(uint1Settings, "0 22 255")); + assertEquals("0 0 0 22 -1 -1", convertText(uint2Settings, "0 22 65535")); + assertEquals("0 0 0 22 -1 -1 -1 -1", convertText(uint4Settings, "22 4294967295")); + assertEquals("22", convertText(uint8Settings, "22")); + assertEquals("-1 -1 -1 -1 -1 -1 -1 -1", + convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromdoubleSettingss() { + assertEquals("", convertText(doubleSettings, "1.234")); + } + + @Test + public void testConvertTextFromString() { + assertEquals("", convertText(stringSettings, "Hey")); + assertEquals("123", convertText(stringSettings, "123")); + assertEquals("", convertText(regExSettings, "B*B")); + assertEquals("123", convertText(regExSettings, "123")); + } + + private void assertRoundTrip(long expected, String input) { + matcher = parse(input); + byte[] bytes = matcher.getBytes(); + long value = getDecimalFormat().getValue(bytes, 0, settings); + assertEquals(expected, value); + } + + private DecimalSearchFormat getDecimalFormat() { + return (DecimalSearchFormat) format; + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/Int2SearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/Int2SearchFormatTest.java new file mode 100644 index 0000000000..757b59658a --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/Int2SearchFormatTest.java @@ -0,0 +1,190 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class Int2SearchFormatTest extends AbstractSearchFormatTest { + public Int2SearchFormatTest() { + super(SearchFormat.DECIMAL); + settings = settings.withDecimalByteSize(2); + } + + @Test + public void testSimpleCaseBigEndian() { + matcher = parse("1 2 -1"); + assertBytes(0, 1, 0, 2, -1, -1); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testSimpleCaseLittleEndian() { + settings = settings.withBigEndian(false); + matcher = parse("1 2 -1"); + assertBytes(1, 0, 2, 0, -1, -1); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testMinimum() { + long value = Short.MIN_VALUE; + matcher = parse(Long.toString(value)); + assertBytes(0x80, 0); + + value -= 1; + ByteMatcher byteMatcher = format.parse(Long.toString(value), settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [-32768, 32767]", + byteMatcher.getDescription()); + } + + @Test + public void testMaximum() { + long value = Short.MAX_VALUE; + matcher = parse(Long.toString(value)); + assertBytes(0x7f, 0xff); + + value += 1; + ByteMatcher byteMatcher = format.parse(Long.toString(value), settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [-32768, 32767]", + byteMatcher.getDescription()); + } + + @Test + public void testNegativeSignOnly() { + ByteMatcher byteMatcher = format.parse("-", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete negative number", byteMatcher.getDescription()); + } + + @Test + public void testBadChars() { + ByteMatcher byteMatcher = format.parse("12z", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Number parse error: For input string: \"12z\"", byteMatcher.getDescription()); + } + + @Test + public void testGetValueBigEndian() { + settings = settings.withBigEndian(true); + assertRoundTrip(-1, "-1"); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(Short.MAX_VALUE, Integer.toString(Short.MAX_VALUE)); + assertRoundTrip(Short.MIN_VALUE, Integer.toString(Short.MIN_VALUE)); + } + + @Test + public void testGetValueLittleEndian() { + settings = settings.withBigEndian(false); + assertRoundTrip(-1, "-1"); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(Short.MAX_VALUE, Integer.toString(Short.MAX_VALUE)); + assertRoundTrip(Short.MIN_VALUE, Integer.toString(Short.MIN_VALUE)); + } + + @Test + public void testCompareValuesBigEndian() { + settings = settings.withBigEndian(true); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes("-10", "-10")); + assertEquals(1, compareBytes("-9", "-10")); + assertEquals(-1, compareBytes("-11", "-10")); + + } + + @Test + public void testCompareValuesLittleEndian() { + settings = settings.withBigEndian(false); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes("-10", "-10")); + assertEquals(1, compareBytes("-9", "-10")); + assertEquals(-1, compareBytes("-11", "-10")); + + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("-30465", convertText(binarySettings, "10001000 11111111")); + assertEquals("256", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromHex() { + assertEquals("22034", convertText(hexSettings, "56 12")); + assertEquals("256", convertText(hexSettings, "1 0")); + } + + @Test + public void testConvertTextFromOtherSizedInts() { + assertEquals("22 -14", convertText(int1Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int4Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int8Settings, "0 22 -1 -14")); + + assertEquals("305 11520", convertText(int4Settings, "20000000")); + assertEquals("0 465 -22198 8192", convertText(int8Settings, "2000000000000")); + + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("22 255", convertText(uint1Settings, "0 22 255")); + assertEquals("0 22 -1", convertText(uint2Settings, "0 22 65535")); + assertEquals("0 22 -1 -1", convertText(uint4Settings, "22 4294967295")); + assertEquals("22", convertText(uint8Settings, "22")); + assertEquals("-1 -1 -1 -1", + convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromDoubles() { + assertEquals("", convertText(doubleSettings, "1.234")); + } + + @Test + public void testConvertTextFromString() { + assertEquals("", convertText(stringSettings, "Hey")); + assertEquals("123", convertText(stringSettings, "123")); + assertEquals("", convertText(regExSettings, "B*B")); + assertEquals("123", convertText(regExSettings, "123")); + } + + private void assertRoundTrip(long expected, String input) { + matcher = parse(input); + byte[] bytes = matcher.getBytes(); + long value = getNumberFormat().getValue(bytes, 0, settings); + assertEquals(expected, value); + } + + private DecimalSearchFormat getNumberFormat() { + return (DecimalSearchFormat) format; + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/Int4SearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/Int4SearchFormatTest.java new file mode 100644 index 0000000000..8f7e1711ae --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/Int4SearchFormatTest.java @@ -0,0 +1,188 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class Int4SearchFormatTest extends AbstractSearchFormatTest { + public Int4SearchFormatTest() { + super(SearchFormat.DECIMAL); + settings = settings.withDecimalByteSize(4); + } + + @Test + public void testSimpleCaseBigEndian() { + matcher = parse("1 2 -1"); + assertBytes(0, 0, 0, 1, 0, 0, 0, 2, -1, -1, -1, -1); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testSimpleCaseLittleEndian() { + settings = settings.withBigEndian(false); + matcher = parse("1 2 -1"); + assertBytes(1, 0, 0, 0, 2, 0, 0, 0, -1, -1, -1, -1); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testMinimum() { + long value = Integer.MIN_VALUE; + matcher = parse(Long.toString(value)); + assertBytes(0x80, 0, 0, 0); + + value -= 1; + ByteMatcher byteMatcher = format.parse(Long.toString(value), settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [-2147483648, 2147483647]", + byteMatcher.getDescription()); + } + + @Test + public void testMaximum() { + long value = Integer.MAX_VALUE; + matcher = parse(Long.toString(value)); + assertBytes(0x7f, 0xff, 0xff, 0xff); + + value += 1; + ByteMatcher byteMatcher = format.parse(Long.toString(value), settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [-2147483648, 2147483647]", + byteMatcher.getDescription()); + } + + @Test + public void testNegativeSignOnly() { + ByteMatcher byteMatcher = format.parse("-", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete negative number", byteMatcher.getDescription()); + } + + @Test + public void testBadChars() { + ByteMatcher byteMatcher = format.parse("12z", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Number parse error: For input string: \"12z\"", byteMatcher.getDescription()); + } + + @Test + public void testGetValueBigEndian() { + settings = settings.withBigEndian(true); + assertRoundTrip(-1, "-1"); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(Integer.MAX_VALUE, Integer.toString(Integer.MAX_VALUE)); + assertRoundTrip(Integer.MIN_VALUE, Integer.toString(Integer.MIN_VALUE)); + } + + @Test + public void testGetValueLittleEndian() { + settings = settings.withBigEndian(false); + assertRoundTrip(-1, "-1"); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(Integer.MAX_VALUE, Integer.toString(Integer.MAX_VALUE)); + assertRoundTrip(Integer.MIN_VALUE, Integer.toString(Integer.MIN_VALUE)); + } + + @Test + public void testCompareValuesBigEndian() { + settings = settings.withBigEndian(true); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes("-10", "-10")); + assertEquals(1, compareBytes("-9", "-10")); + assertEquals(-1, compareBytes("-11", "-10")); + + } + + @Test + public void testCompareValuesLittleEndian() { + settings = settings.withBigEndian(false); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes("-10", "-10")); + assertEquals(1, compareBytes("-9", "-10")); + assertEquals(-1, compareBytes("-11", "-10")); + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("35071", convertText(binarySettings, "10001000 11111111")); + assertEquals("256", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromHex() { + assertEquals("22034", convertText(hexSettings, "56 12")); + assertEquals("256", convertText(hexSettings, "1 0")); + } + + @Test + public void testConvertTextFromOtherSizedInts() { + assertEquals("1507314", convertText(int1Settings, "0 22 -1 -14")); + assertEquals("22 -14", convertText(int2Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int8Settings, "0 22 -1 -14")); + + assertEquals("20000000", convertText(int4Settings, "20000000")); + assertEquals("465 -1454759936", convertText(int8Settings, "2000000000000")); + + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("5887", convertText(uint1Settings, "0 22 255")); + assertEquals("22 65535", convertText(uint2Settings, "0 22 65535")); + assertEquals("22 -1", convertText(uint4Settings, "22 4294967295")); + assertEquals("22", convertText(uint8Settings, "22")); + assertEquals("-1 -1", + convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromDoubles() { + assertEquals("", convertText(doubleSettings, "1.234")); + } + + @Test + public void testConvertTextFromString() { + assertEquals("", convertText(stringSettings, "Hey")); + assertEquals("123", convertText(stringSettings, "123")); + assertEquals("", convertText(regExSettings, "B*B")); + assertEquals("123", convertText(regExSettings, "123")); + } + + private void assertRoundTrip(long expected, String input) { + matcher = parse(input); + byte[] bytes = matcher.getBytes(); + long value = getDecimalFormat().getValue(bytes, 0, settings); + assertEquals(expected, value); + } + + private DecimalSearchFormat getDecimalFormat() { + return (DecimalSearchFormat) format; + } +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/Int8SearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/Int8SearchFormatTest.java new file mode 100644 index 0000000000..982e7bea90 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/Int8SearchFormatTest.java @@ -0,0 +1,197 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import java.math.BigInteger; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class Int8SearchFormatTest extends AbstractSearchFormatTest { + public Int8SearchFormatTest() { + super(SearchFormat.DECIMAL); + settings = settings.withDecimalByteSize(8); + + } + + @Test + public void testSimpleCaseBigEndian() { + matcher = parse("1 2 -1"); + assertBytes(0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testSimpleCaseLittleEndian() { + settings = settings.withBigEndian(false); + matcher = parse("1 2 -1"); + assertBytes(1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testMinimum() { + long value = Long.MIN_VALUE; + matcher = parse(Long.toString(value)); + assertBytes(0x80, 0, 0, 0, 0, 0, 0, 0); + + BigInteger bigValue = BigInteger.valueOf(value).subtract(BigInteger.ONE); + ByteMatcher byteMatcher = format.parse(bigValue.toString(), settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [-9223372036854775808, 9223372036854775807]", + byteMatcher.getDescription()); + } + + @Test + public void testMaximum() { + long value = Long.MAX_VALUE; + matcher = parse(Long.toString(value)); + assertBytes(0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + + BigInteger bigValue = BigInteger.valueOf(value).add(BigInteger.ONE); + ByteMatcher byteMatcher = format.parse(bigValue.toString(), settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [-9223372036854775808, 9223372036854775807]", + byteMatcher.getDescription()); + } + + @Test + public void testNegativeSignOnly() { + ByteMatcher byteMatcher = format.parse("-", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Incomplete negative number", byteMatcher.getDescription()); + } + + @Test + public void testBadChars() { + ByteMatcher byteMatcher = format.parse("12z", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Number parse error: For input string: \"12z\"", byteMatcher.getDescription()); + } + + @Test + public void testGetValueBigEndian() { + settings = settings.withBigEndian(true); + assertRoundTrip(-1, "-1"); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(Long.MAX_VALUE, Long.toString(Long.MAX_VALUE)); + assertRoundTrip(Long.MIN_VALUE, Long.toString(Long.MIN_VALUE)); + } + + @Test + public void testGetValueLittleEndian() { + settings = settings.withBigEndian(false); + assertRoundTrip(-1, "-1"); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(Long.MAX_VALUE, Long.toString(Long.MAX_VALUE)); + assertRoundTrip(Long.MIN_VALUE, Long.toString(Long.MIN_VALUE)); + } + + @Test + public void testCompareValuesBigEndian() { + settings = settings.withBigEndian(true); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes("-10", "-10")); + assertEquals(1, compareBytes("-9", "-10")); + assertEquals(-1, compareBytes("-11", "-10")); + + assertEquals(0, compareBytes(str(Long.MAX_VALUE), str(Long.MAX_VALUE))); + assertEquals(0, compareBytes(str(Long.MIN_VALUE), str(Long.MIN_VALUE))); + assertEquals(-1, compareBytes(str(Long.MIN_VALUE), str(Long.MAX_VALUE))); + assertEquals(1, compareBytes(str(Long.MAX_VALUE), str(Long.MIN_VALUE))); + } + + @Test + public void testCompareValuesLittleEndian() { + settings = settings.withBigEndian(false); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes("-10", "-10")); + assertEquals(1, compareBytes("-9", "-10")); + assertEquals(-1, compareBytes("-11", "-10")); + + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("35071", convertText(binarySettings, "10001000 11111111")); + assertEquals("256", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromHex() { + assertEquals("22034", convertText(hexSettings, "56 12")); + assertEquals("256", convertText(hexSettings, "1 0")); + } + + @Test + public void testConvertTextFromOtherSizedInts() { + assertEquals("1507314", convertText(int1Settings, "0 22 -1 -14")); + assertEquals("98784247794", convertText(int2Settings, "0 22 -1 -14")); + assertEquals("22 -14", convertText(int4Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int8Settings, "0 22 -1 -14")); + + assertEquals("20000000", convertText(int4Settings, "20000000")); + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("5887", convertText(uint1Settings, "0 22 255")); + assertEquals("1507327", convertText(uint2Settings, "0 22 65535")); + assertEquals("98784247807", convertText(uint4Settings, "22 4294967295")); + assertEquals("22", convertText(uint8Settings, "22")); + assertEquals("-1", convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromDoubles() { + assertEquals("", convertText(doubleSettings, "1.234")); + } + + @Test + public void testConvertTextFromString() { + assertEquals("", convertText(stringSettings, "Hey")); + assertEquals("123", convertText(stringSettings, "123")); + assertEquals("", convertText(regExSettings, "B*B")); + assertEquals("123", convertText(regExSettings, "123")); + } + + private void assertRoundTrip(long expected, String input) { + matcher = parse(input); + byte[] bytes = matcher.getBytes(); + long value = getNumberFormat().getValue(bytes, 0, settings); + assertEquals(expected, value); + } + + private DecimalSearchFormat getNumberFormat() { + return (DecimalSearchFormat) format; + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/RegExSearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/RegExSearchFormatTest.java new file mode 100644 index 0000000000..7f2af90a3f --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/RegExSearchFormatTest.java @@ -0,0 +1,86 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class RegExSearchFormatTest extends AbstractSearchFormatTest { + private ByteMatcher byteMatcher; + + public RegExSearchFormatTest() { + super(SearchFormat.REG_EX); + } + + @Test + public void testSimpleCase() { + byteMatcher = format.parse("a|b", settings); + + assertTrue(byteMatcher.isValidInput()); + assertTrue(byteMatcher.isValidSearch()); + assertEquals("Reg Ex", byteMatcher.getDescription()); + } + + @Test + public void testIncompleteCase() { + byteMatcher = format.parse("a(", settings); + assertTrue(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("RegEx Pattern Error: Unclosed group", byteMatcher.getDescription()); + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("10001000 11111111", convertText(binarySettings, "10001000 11111111")); + assertEquals("1 0", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromHex() { + assertEquals("56 12", convertText(hexSettings, "56 12")); + assertEquals("1 0", convertText(hexSettings, "1 0")); + } + + @Test + public void testConvertTextFromInts() { + assertEquals("0 22 -1 -14", convertText(int1Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int2Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int8Settings, "0 22 -1 -14")); + + assertEquals("20000000", convertText(int4Settings, "20000000")); + assertEquals("2000000000000", convertText(int8Settings, "2000000000000")); + + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("0 22 255", convertText(uint1Settings, "0 22 255")); + assertEquals("0 22 65535", convertText(uint2Settings, "0 22 65535")); + assertEquals("22 4294967295", convertText(uint4Settings, "22 4294967295")); + assertEquals("22", convertText(uint8Settings, "22")); + assertEquals("18446744073709551615", + convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromDoubles() { + assertEquals("1.234", convertText(doubleSettings, "1.234")); + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/StringSearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/StringSearchFormatTest.java new file mode 100644 index 0000000000..dfc94da9b6 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/StringSearchFormatTest.java @@ -0,0 +1,165 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import java.nio.charset.StandardCharsets; + +import org.junit.Test; + +public class StringSearchFormatTest extends AbstractSearchFormatTest { + + public StringSearchFormatTest() { + super(SearchFormat.STRING); + settings = settings.withCaseSensitive(true); + settings = settings.withUseEscapeSequence(false); + } + + @Test + public void testCaseSensitive() { + matcher = parse("aBc12"); + + assertBytes(0x61, 0x42, 0x63, 0x31, 0x32); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testCaseInSensitriveUTF8() { + settings = settings.withCaseSensitive(false); + matcher = parse("aBc12"); + + assertBytes(0x41, 0x42, 0x43, 0x31, 0x32); + assertMask(0xdf, 0xdf, 0xdf, 0xff, 0xff); + } + + @Test + public void testCaseSensitiveUTF8() { + settings = settings.withStringCharset(StandardCharsets.UTF_8); + matcher = parse("aBc12"); + + assertBytes(0x61, 0x42, 0x63, 0x31, 0x32); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testCaseInSensitrive() { + settings = settings.withCaseSensitive(false); + matcher = parse("aBc12"); + + assertBytes(0x41, 0x42, 0x43, 0x31, 0x32); + assertMask(0xdf, 0xdf, 0xdf, 0xff, 0xff); + } + + @Test + public void testEscapeSequence() { + settings = settings.withUseEscapeSequence(false); + matcher = parse("a\\n"); + assertBytes(0x61, 0x5c, 0x6e); + + settings = settings.withUseEscapeSequence(true); + matcher = parse("a\\n"); + assertBytes(0x61, 0x0a); + + } + + @Test + public void testUTF16CaseSensitiveLittleEndian() { + settings = settings.withBigEndian(false); + settings = settings.withStringCharset(StandardCharsets.UTF_16); + matcher = parse("aBc12"); + + assertBytes(0x61, 0x0, 0x42, 0x0, 0x63, 0x0, 0x31, 0x0, 0x32, 0x0); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testUTF16CaseSensitiveBigEndian() { + settings = settings.withBigEndian(true); + settings = settings.withStringCharset(StandardCharsets.UTF_16); + matcher = parse("aBc12"); + + assertBytes(0x00, 0x61, 0x0, 0x42, 0x0, 0x63, 0x0, 0x31, 0x0, 0x32); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testUTF16CaseInSensitiveLittleEndian() { + settings = settings.withCaseSensitive(false); + settings = settings.withBigEndian(false); + settings = settings.withStringCharset(StandardCharsets.UTF_16); + matcher = parse("aBc12"); + + assertBytes(0x41, 0x0, 0x42, 0x0, 0x43, 0x0, 0x31, 0x0, 0x32, 0x0); + assertMask(0xdf, 0xff, 0xdf, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testUTF16CaseInSensitiveBigEndian() { + settings = settings.withCaseSensitive(false); + settings = settings.withBigEndian(true); + settings = settings.withStringCharset(StandardCharsets.UTF_16); + matcher = parse("aBc12"); + + assertBytes(0x00, 0x41, 0x0, 0x42, 0x0, 0x43, 0x0, 0x31, 0x0, 0x32); + assertMask(0xff, 0xdf, 0xff, 0xdf, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("10001000 11111111", convertText(binarySettings, "10001000 11111111")); + assertEquals("1 0", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromHex() { + assertEquals("56 12", convertText(hexSettings, "56 12")); + assertEquals("1 0", convertText(hexSettings, "1 0")); + } + + @Test + public void testConvertTextFromInts() { + assertEquals("0 22 -1 -14", convertText(int1Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int2Settings, "0 22 -1 -14")); + assertEquals("0 22 -1 -14", convertText(int8Settings, "0 22 -1 -14")); + + assertEquals("20000000", convertText(int4Settings, "20000000")); + assertEquals("2000000000000", convertText(int8Settings, "2000000000000")); + + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("0 22 255", convertText(uint1Settings, "0 22 255")); + assertEquals("0 22 65535", convertText(uint2Settings, "0 22 65535")); + assertEquals("22 4294967295", convertText(uint4Settings, "22 4294967295")); + assertEquals("22", convertText(uint8Settings, "22")); + assertEquals("18446744073709551615", + convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromDoubles() { + assertEquals("1.234", convertText(doubleSettings, "1.234")); + } + + protected void assertStringBytes(String string) { + byte[] bytes = matcher.getBytes(); + byte[] expectedBytes = string.getBytes(); + assertArrayEquals(expectedBytes, bytes); + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/UInt1SearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/UInt1SearchFormatTest.java new file mode 100644 index 0000000000..6dfa0d87d9 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/UInt1SearchFormatTest.java @@ -0,0 +1,189 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class UInt1SearchFormatTest extends AbstractSearchFormatTest { + public UInt1SearchFormatTest() { + super(SearchFormat.DECIMAL); + settings = settings.withDecimalByteSize(1); + settings = settings.withDecimalUnsigned(true); + } + + @Test + public void testSimpleCaseBigEndian() { + matcher = parse("1 2 3"); + assertBytes(1, 2, 3); + assertMask(0xff, 0xff, 0xff); + } + + @Test + public void testSimpleCaseLittleEndian() { + settings = settings.withBigEndian(false); + matcher = parse("1 2 3"); + assertBytes(1, 2, 3); + assertMask(0xff, 0xff, 0xff); + } + + @Test + public void testMinimum() { + matcher = parse("0"); + assertBytes(0); + + ByteMatcher byteMatcher = format.parse("-1", settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [0, 255]", + byteMatcher.getDescription()); + } + + @Test + public void testMaximum() { + long value = 0xffL; + matcher = parse(Long.toString(value)); + assertBytes(0xff); + + value += 1; + ByteMatcher byteMatcher = format.parse(Long.toString(value), settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [0, 255]", + byteMatcher.getDescription()); + } + + @Test + public void testNegativeSignOnly() { + ByteMatcher byteMatcher = format.parse("-", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Negative numbers not allowed for unsigned values", + byteMatcher.getDescription()); + } + + @Test + public void testBadChars() { + ByteMatcher byteMatcher = format.parse("12z", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Number parse error: For input string: \"12z\"", byteMatcher.getDescription()); + } + + @Test + public void testGetValueBigEndian() { + settings = settings.withBigEndian(true); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(0xffL, Long.toString(0xffL)); + } + + @Test + public void testGetValueLittleEndian() { + settings = settings.withBigEndian(false); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(0xffL, Long.toString(0xffL)); + } + + @Test + public void testCompareValuesBigEndian() { + settings = settings.withBigEndian(true); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes(str(0xffl), str(0xffL))); + assertEquals(1, compareBytes(str(0xffl), str(0xfeL))); + assertEquals(-1, compareBytes(str(0xfeL), str(0xffL))); + + } + + @Test + public void testCompareValuesLittleEndian() { + settings = settings.withBigEndian(false); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes(str(0xffL), str(0xffL))); + assertEquals(1, compareBytes(str(0xffL), str(0xfeL))); + assertEquals(-1, compareBytes(str(0xfeL), str(0xffL))); + + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("136 255", convertText(binarySettings, "10001000 11111111")); + assertEquals("1 0", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromHex() { + assertEquals("86 18", convertText(hexSettings, "56 12")); + assertEquals("1 0", convertText(hexSettings, "1 0")); + } + + @Test + public void testConvertTextFromOtherSizedInts() { + assertEquals("0 0 0 22 255 255 255 242", convertText(int2Settings, "0 22 -1 -14")); + assertEquals("0 0 0 0 0 0 0 22 255 255 255 255", + convertText(int4Settings, "0 22 -1")); + assertEquals("0", convertText(int8Settings, "0")); + assertEquals("255 255 255 255 255 255 255 255", convertText(int8Settings, "-1")); + + assertEquals("78 32", convertText(int2Settings, "20000")); + assertEquals("1 49 45 0", convertText(int4Settings, "20000000")); + assertEquals("0 0 1 209 169 74 32 0", convertText(int8Settings, "2000000000000")); + + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("0 22 255", convertText(uint1Settings, "0 22 255")); + assertEquals("0 0 0 22 255 255", convertText(uint2Settings, "0 22 65535")); + assertEquals("0 0 0 22 255 255 255 255", convertText(uint4Settings, "22 4294967295")); + assertEquals("22", convertText(uint8Settings, "22")); + assertEquals("255 255 255 255 255 255 255 255", + convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromDoubles() { + assertEquals("", convertText(doubleSettings, "1.234")); + } + + @Test + public void testConvertTextFromString() { + assertEquals("", convertText(stringSettings, "Hey")); + assertEquals("123", convertText(stringSettings, "123")); + assertEquals("", convertText(regExSettings, "B*B")); + assertEquals("123", convertText(regExSettings, "123")); + } + + private void assertRoundTrip(long expected, String input) { + matcher = parse(input); + byte[] bytes = matcher.getBytes(); + long value = getNumberFormat().getValue(bytes, 0, settings); + assertEquals(expected, value); + } + + private DecimalSearchFormat getNumberFormat() { + return (DecimalSearchFormat) format; + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/UInt2SearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/UInt2SearchFormatTest.java new file mode 100644 index 0000000000..0ed64f5d82 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/UInt2SearchFormatTest.java @@ -0,0 +1,186 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class UInt2SearchFormatTest extends AbstractSearchFormatTest { + public UInt2SearchFormatTest() { + super(SearchFormat.DECIMAL); + settings = settings.withDecimalByteSize(2); + settings = settings.withDecimalUnsigned(true); + } + + @Test + public void testSimpleCaseBigEndian() { + matcher = parse("1 2 3"); + assertBytes(0, 1, 0, 2, 0, 3); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testSimpleCaseLittleEndian() { + settings = settings.withBigEndian(false); + matcher = parse("1 2 3"); + assertBytes(1, 0, 2, 0, 3, 0); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testMinimum() { + matcher = parse("0"); + assertBytes(0, 0); + + ByteMatcher byteMatcher = format.parse("-1", settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [0, 65535]", + byteMatcher.getDescription()); + } + + @Test + public void testMaximum() { + long value = 0xffffL; + matcher = parse(Long.toString(value)); + assertBytes(0xff, 0xff); + + value += 1; + ByteMatcher byteMatcher = format.parse(Long.toString(value), settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [0, 65535]", + byteMatcher.getDescription()); + } + + @Test + public void testNegativeSignOnly() { + ByteMatcher byteMatcher = format.parse("-", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Negative numbers not allowed for unsigned values", + byteMatcher.getDescription()); + } + + @Test + public void testBadChars() { + ByteMatcher byteMatcher = format.parse("12z", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Number parse error: For input string: \"12z\"", byteMatcher.getDescription()); + } + + @Test + public void testGetValueBigEndian() { + settings = settings.withBigEndian(true); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(0xffffL, Long.toString(0xffffL)); + } + + @Test + public void testGetValueLittleEndian() { + settings = settings.withBigEndian(false); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(0xffffL, Long.toString(0xffffL)); + } + + @Test + public void testCompareValuesBigEndian() { + settings = settings.withBigEndian(true); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes(str(0xffffl), str(0xffffL))); + assertEquals(1, compareBytes(str(0xffffl), str(0xfffeL))); + assertEquals(-1, compareBytes(str(0xfffeL), str(0xffffL))); + + } + + @Test + public void testCompareValuesLittleEndian() { + settings = settings.withBigEndian(false); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes(str(0xffffL), str(0xffffL))); + assertEquals(1, compareBytes(str(0xffffL), str(0xfffeL))); + assertEquals(-1, compareBytes(str(0xfffeL), str(0xffffL))); + + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("35071", convertText(binarySettings, "10001000 11111111")); + assertEquals("256", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromHex() { + assertEquals("22034", convertText(hexSettings, "56 12")); + assertEquals("256", convertText(hexSettings, "1 0")); + } + + @Test + public void testConvertTextFromOtherSizedInts() { + assertEquals("22 65522", convertText(int1Settings, "0 22 -1 -14")); + assertEquals("0 22 65535 65535", convertText(int4Settings, "22 -1")); + assertEquals("0 0 0 22 65535 65535 65535 65535", convertText(int8Settings, "22 -1")); + + assertEquals("305 11520", convertText(int4Settings, "20000000")); + assertEquals("0 465 43338 8192", convertText(int8Settings, "2000000000000")); + + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("22 255", convertText(uint1Settings, "0 22 255")); + assertEquals("0 22 65535", convertText(uint2Settings, "0 22 65535")); + assertEquals("0 22 65535 65535", convertText(uint4Settings, "22 4294967295")); + assertEquals("22", convertText(uint8Settings, "22")); + assertEquals("65535 65535 65535 65535", + convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromDoubles() { + assertEquals("", convertText(doubleSettings, "1.234")); + } + + @Test + public void testConvertTextFromString() { + assertEquals("", convertText(stringSettings, "Hey")); + assertEquals("123", convertText(stringSettings, "123")); + assertEquals("", convertText(regExSettings, "B*B")); + assertEquals("123", convertText(regExSettings, "123")); + } + + private void assertRoundTrip(long expected, String input) { + matcher = parse(input); + byte[] bytes = matcher.getBytes(); + long value = getNumberFormat().getValue(bytes, 0, settings); + assertEquals(expected, value); + } + + private DecimalSearchFormat getNumberFormat() { + return (DecimalSearchFormat) format; + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/UInt4SearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/UInt4SearchFormatTest.java new file mode 100644 index 0000000000..34051186f8 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/UInt4SearchFormatTest.java @@ -0,0 +1,187 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class UInt4SearchFormatTest extends AbstractSearchFormatTest { + public UInt4SearchFormatTest() { + super(SearchFormat.DECIMAL); + settings = settings.withDecimalByteSize(4); + settings = settings.withDecimalUnsigned(true); + } + + @Test + public void testSimpleCaseBigEndian() { + matcher = parse("1 2 3"); + assertBytes(0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testSimpleCaseLittleEndian() { + settings = settings.withBigEndian(false); + matcher = parse("1 2 3"); + assertBytes(1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testMinimum() { + matcher = parse("0"); + assertBytes(0, 0, 0, 0); + + ByteMatcher byteMatcher = format.parse("-1", settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [0, 4294967295]", + byteMatcher.getDescription()); + } + + @Test + public void testMaximum() { + long value = 0xffffffffL; + matcher = parse(Long.toString(value)); + assertBytes(0xff, 0xff, 0xff, 0xff); + + value += 1; + ByteMatcher byteMatcher = format.parse(Long.toString(value), settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [0, 4294967295]", + byteMatcher.getDescription()); + } + + @Test + public void testNegativeSignOnly() { + ByteMatcher byteMatcher = format.parse("-", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Negative numbers not allowed for unsigned values", + byteMatcher.getDescription()); + } + + @Test + public void testBadChars() { + ByteMatcher byteMatcher = format.parse("12z", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Number parse error: For input string: \"12z\"", byteMatcher.getDescription()); + } + + @Test + public void testGetValueBigEndian() { + settings = settings.withBigEndian(true); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(0xffffffffL, Long.toString(0xffffffffL)); + } + + @Test + public void testGetValueLittleEndian() { + settings = settings.withBigEndian(false); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(0xffffffffL, Long.toString(0xffffffffL)); + } + + @Test + public void testCompareValuesBigEndian() { + settings = settings.withBigEndian(true); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes(str(0xffffffffl), str(0xffffffffL))); + assertEquals(1, compareBytes(str(0xffffffffl), str(0xfffffffeL))); + assertEquals(-1, compareBytes(str(0xfffffffeL), str(0xffffffffL))); + + } + + @Test + public void testCompareValuesLittleEndian() { + settings = settings.withBigEndian(false); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + assertEquals(0, compareBytes(str(0xffffffffL), str(0xffffffffL))); + assertEquals(1, compareBytes(str(0xffffffffL), str(0xfffffffeL))); + assertEquals(-1, compareBytes(str(0xfffffffeL), str(0xffffffffL))); + + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("35071", convertText(binarySettings, "10001000 11111111")); + assertEquals("256", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromHex() { + assertEquals("22034", convertText(hexSettings, "56 12")); + assertEquals("256", convertText(hexSettings, "1 0")); + } + + @Test + public void testConvertTextFromOtherSizedInts() { + assertEquals("1507314", convertText(int1Settings, "0 22 -1 -14")); + assertEquals("22 4294967282", convertText(int2Settings, "0 22 -1 -14")); + assertEquals("0 22 4294967295 4294967282", convertText(int4Settings, "0 22 -1 -14")); + assertEquals("0 22 4294967295 4294967295", convertText(int8Settings, "22 -1")); + + assertEquals("20000000", convertText(int4Settings, "20000000")); + assertEquals("465 2840207360", convertText(int8Settings, "2000000000000")); + + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("5887", convertText(uint1Settings, "0 22 255")); + assertEquals("22 65535", convertText(uint2Settings, "0 22 65535")); + assertEquals("22 4294967295", convertText(uint4Settings, "22 4294967295")); + assertEquals("22", convertText(uint8Settings, "22")); + assertEquals("4294967295 4294967295", + convertText(uint8Settings, "18446744073709551615")); + } + + @Test + public void testConvertTextFromDoubles() { + assertEquals("", convertText(doubleSettings, "1.234")); + } + + @Test + public void testConvertTextFromString() { + assertEquals("", convertText(stringSettings, "Hey")); + assertEquals("123", convertText(stringSettings, "123")); + assertEquals("", convertText(regExSettings, "B*B")); + assertEquals("123", convertText(regExSettings, "123")); + } + + private void assertRoundTrip(long expected, String input) { + matcher = parse(input); + byte[] bytes = matcher.getBytes(); + long value = getNumberFormat().getValue(bytes, 0, settings); + assertEquals(expected, value); + } + + private DecimalSearchFormat getNumberFormat() { + return (DecimalSearchFormat) format; + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/UInt8SearchFormatTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/UInt8SearchFormatTest.java new file mode 100644 index 0000000000..2ba5142353 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/format/UInt8SearchFormatTest.java @@ -0,0 +1,192 @@ +/* ### + * 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.features.base.memsearch.format; + +import static org.junit.Assert.*; + +import java.math.BigInteger; + +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.ByteMatcher; + +public class UInt8SearchFormatTest extends AbstractSearchFormatTest { + public UInt8SearchFormatTest() { + super(SearchFormat.DECIMAL); + settings = settings.withDecimalByteSize(8); + settings = settings.withDecimalUnsigned(true); + } + + @Test + public void testSimpleCaseBigEndian() { + matcher = parse("1 2 3"); + assertBytes(0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testSimpleCaseLittleEndian() { + settings = settings.withBigEndian(false); + matcher = parse("1 2 3"); + assertBytes(1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0); + assertMask(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + } + + @Test + public void testMinimum() { + matcher = parse("0"); + assertBytes(0, 0, 0, 0, 0, 0, 0, 0); + + ByteMatcher byteMatcher = format.parse("-1", settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [0, 18446744073709551615]", + byteMatcher.getDescription()); + } + + @Test + public void testMaximum() { + BigInteger value = new BigInteger("ffffffffffffffff", 16); + matcher = parse(value.toString()); + assertBytes(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + + BigInteger bigValue = value.add(BigInteger.ONE); + ByteMatcher byteMatcher = format.parse(bigValue.toString(), settings); + assertFalse(byteMatcher.isValidInput()); + assertEquals("Number must be in the range [0, 18446744073709551615]", + byteMatcher.getDescription()); + + } + + @Test + public void testNegativeSignOnly() { + ByteMatcher byteMatcher = format.parse("-", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Negative numbers not allowed for unsigned values", + byteMatcher.getDescription()); + } + + @Test + public void testBadChars() { + ByteMatcher byteMatcher = format.parse("12z", settings); + assertFalse(byteMatcher.isValidInput()); + assertFalse(byteMatcher.isValidSearch()); + assertEquals("Number parse error: For input string: \"12z\"", byteMatcher.getDescription()); + } + + @Test + public void testGetValueBigEndian() { + settings = settings.withBigEndian(true); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(-1, new BigInteger("ffffffffffffffff", 16).toString()); + } + + @Test + public void testGetValueLittleEndian() { + settings = settings.withBigEndian(false); + assertRoundTrip(1, "1"); + assertRoundTrip(0, "0"); + assertRoundTrip(-1, new BigInteger("ffffffffffffffff", 16).toString()); + } + + @Test + public void testCompareValuesBigEndian() { + settings = settings.withBigEndian(true); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + BigInteger maxValue = new BigInteger("ffffffffffffffff", 16); + BigInteger maxValueMinus1 = maxValue.subtract(BigInteger.valueOf(1)); + assertEquals(0, compareBytes(maxValue.toString(), maxValue.toString())); + assertEquals(1, compareBytes(maxValue.toString(), maxValueMinus1.toString())); + assertEquals(-1, compareBytes(maxValueMinus1.toString(), maxValue.toString())); + + } + + @Test + public void testCompareValuesLittleEndian() { + settings = settings.withBigEndian(false); + assertEquals(0, compareBytes("10", "10")); + assertEquals(-1, compareBytes("9", "10")); + assertEquals(1, compareBytes("11", "10")); + + BigInteger maxValue = new BigInteger("ffffffffffffffff", 16); + BigInteger maxValueMinus1 = maxValue.subtract(BigInteger.valueOf(1)); + assertEquals(0, compareBytes(maxValue.toString(), maxValue.toString())); + assertEquals(1, compareBytes(maxValue.toString(), maxValueMinus1.toString())); + assertEquals(-1, compareBytes(maxValueMinus1.toString(), maxValue.toString())); + + } + + @Test + public void testConvertTextFromBinary() { + assertEquals("35071", convertText(binarySettings, "10001000 11111111")); + assertEquals("256", convertText(binarySettings, "1 0")); + } + + @Test + public void testConvertTextFromHex() { + assertEquals("22034", convertText(hexSettings, "56 12")); + assertEquals("256", convertText(hexSettings, "1 0")); + } + + @Test + public void testConvertTextFromOtherSizedInts() { + assertEquals("1507314", convertText(int1Settings, "0 22 -1 -14")); + assertEquals("98784247794", convertText(int2Settings, "0 22 -1 -14")); + assertEquals("22 18446744073709551615", convertText(int4Settings, "0 22 -1 -1")); + assertEquals("0 22 18446744073709551615", convertText(int8Settings, "0 22 -1")); + + assertEquals("20000000", convertText(int4Settings, "20000000")); + } + + @Test + public void testConvertTextFromUnsignedInts() { + assertEquals("5887", convertText(uint1Settings, "0 22 255")); + assertEquals("1507327", convertText(uint2Settings, "0 22 65535")); + assertEquals("98784247807", convertText(uint4Settings, "22 4294967295")); + assertEquals("22", convertText(uint8Settings, "22")); + } + + @Test + public void testConvertTextFromDoubles() { + assertEquals("", convertText(doubleSettings, "1.234")); + } + + @Test + public void testConvertTextFromString() { + assertEquals("", convertText(stringSettings, "Hey")); + assertEquals("123", convertText(stringSettings, "123")); + assertEquals("", convertText(regExSettings, "B*B")); + assertEquals("123", convertText(regExSettings, "123")); + } + + private void assertRoundTrip(long expected, String input) { + matcher = parse(input); + byte[] bytes = matcher.getBytes(); + long value = getNumberFormat().getValue(bytes, 0, settings); + assertEquals(expected, value); + } + + private DecimalSearchFormat getNumberFormat() { + return (DecimalSearchFormat) format; + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/searcher/MemSearcherTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/searcher/MemSearcherTest.java new file mode 100644 index 0000000000..1c94a0fb1c --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/searcher/MemSearcherTest.java @@ -0,0 +1,337 @@ +/* ### + * 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.features.base.memsearch.searcher; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.features.base.memsearch.bytesource.AddressableByteSource; +import ghidra.features.base.memsearch.bytesource.SearchRegion; +import ghidra.features.base.memsearch.matcher.ByteMatcher; +import ghidra.features.base.memsearch.matcher.RegExByteMatcher; +import ghidra.program.model.address.*; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.datastruct.ListAccumulator; +import ghidra.util.task.TaskMonitor; + +public class MemSearcherTest { + private static final int SEARCH_LIMIT = 10; + private static final int TINY_CHUNK_SIZE = 4; + private TestByteSource bytes; + private AddressSpace space; + private TaskMonitor monitor = TaskMonitor.DUMMY; + private ByteMatcher bobMatcher = new RegExByteMatcher("bob", null); + private Accumulator accumulator = new ListAccumulator<>(); + + @Before + public void setUp() { + space = new GenericAddressSpace("test", 64, AddressSpace.TYPE_RAM, 0); + } + + @Test + public void testFindNext() { + bytes = new TestByteSource(addr(0), "xxbobxxx"); + MemorySearcher searcher = new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT); + + MemoryMatch match = searcher.findNext(addr(0), monitor); + assertMatch(2, "bob", match); + } + + @Test + public void testFindNextStartingAtMatch() { + bytes = new TestByteSource(addr(0), "xxbobxxx"); + MemorySearcher searcher = new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT); + + MemoryMatch match = searcher.findNext(addr(2), monitor); + assertMatch(2, "bob", match); + } + + @Test + public void testFindNextNoMatch() { + bytes = new TestByteSource(addr(0), "xxjoexxx"); + MemorySearcher searcher = new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT); + + MemoryMatch match = searcher.findNext(addr(0), monitor); + assertNull(match); + } + + @Test + public void testFindNextInSecondChunk() { + bytes = new TestByteSource(addr(0), "xxxx xbob x"); // spaces are removed by bytes call + AddressSet addresses = bytes.getAddressSet(); + MemorySearcher searcher = + new MemorySearcher(bytes, bobMatcher, addresses, SEARCH_LIMIT, TINY_CHUNK_SIZE); + + MemoryMatch match = searcher.findNext(addr(0), monitor); + assertMatch(5, "bob", match); + } + + @Test + public void testFindNextInLaterChunk() { + bytes = new TestByteSource(addr(0), "xxxx xxxx xxxx xxxx xbob x"); + MemorySearcher searcher = + new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT, TINY_CHUNK_SIZE); + + MemoryMatch match = searcher.findNext(addr(0), monitor); + assertMatch(17, "bob", match); + } + + @Test + public void testFindNextMatchSpansChunks() { + bytes = new TestByteSource(addr(0), "xxxb obxx"); + MemorySearcher searcher = + new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT, TINY_CHUNK_SIZE); + + MemoryMatch match = searcher.findNext(addr(0), monitor); + assertMatch(3, "bob", match); + } + + @Test + public void testFindNextMultipleRanges() { + bytes = new TestByteSource(addr(0), "xxxxx"); + bytes.addBytes(addr(100), "xxxxxboxxbxx"); + bytes.addBytes(addr(200), "xxxbobxxxxbobxxxx"); + MemorySearcher searcher = new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT); + + MemoryMatch match = searcher.findNext(addr(0), monitor); + assertMatch(203, "bob", match); + } + + @Test + public void testFindPrevious() { + bytes = new TestByteSource(addr(0), "xxbobxxx"); + MemorySearcher searcher = new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT); + + MemoryMatch match = searcher.findPrevious(addr(100), monitor); + assertMatch(2, "bob", match); + } + + @Test + public void testFindPreviousNoMatch() { + bytes = new TestByteSource(addr(0), "xxjoexxx"); + MemorySearcher searcher = new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT); + + MemoryMatch match = searcher.findPrevious(addr(100), monitor); + assertNull(match); + } + + @Test + public void testFindPreviousStartingAtMatch() { + bytes = new TestByteSource(addr(0), "xxbobxxx"); + MemorySearcher searcher = new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT); + + MemoryMatch match = searcher.findPrevious(addr(2), monitor); + assertMatch(2, "bob", match); + } + + @Test + public void testFindPreviousInFirstChunk() { + bytes = new TestByteSource(addr(0), "xxxx xbob"); + MemorySearcher searcher = + new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT, TINY_CHUNK_SIZE); + + MemoryMatch match = searcher.findPrevious(addr(100), monitor); + assertMatch(5, "bob", match); + } + + @Test + public void testFindPreviousInSecondChunk() { + bytes = new TestByteSource(addr(0), "xbob xxxx"); + MemorySearcher searcher = + new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT, TINY_CHUNK_SIZE); + + MemoryMatch match = searcher.findPrevious(addr(100), monitor); + assertMatch(1, "bob", match); + } + + @Test + public void testFindPreviousInLaterChunk() { + bytes = new TestByteSource(addr(0), "xbob xxxx xxxx xxxx xxxx xxxx x"); + MemorySearcher searcher = + new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT, TINY_CHUNK_SIZE); + + MemoryMatch match = searcher.findPrevious(addr(100), monitor); + assertMatch(1, "bob", match); + } + + @Test + public void testFindPreviousSpansChunk() { + bytes = new TestByteSource(addr(0), "xxbo bxxx"); + MemorySearcher searcher = + new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT, TINY_CHUNK_SIZE); + + MemoryMatch match = searcher.findPrevious(addr(100), monitor); + assertMatch(2, "bob", match); + } + + @Test + public void testFindPrevioustMultipleRanges() { + bytes = new TestByteSource(addr(0), "xxbobxxx"); + bytes.addBytes(addr(100), "xxxxxboxxbxx"); + bytes.addBytes(addr(200), "xxxxxxxbbxxxx"); + MemorySearcher searcher = new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT); + + MemoryMatch match = searcher.findNext(addr(0), monitor); + assertMatch(2, "bob", match); + } + + @Test + public void testFindAll() { + bytes = new TestByteSource(addr(0), "xbob xxxb obxx xxxx xxbo b"); + MemorySearcher searcher = + new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT, TINY_CHUNK_SIZE); + searcher.findAll(accumulator, monitor); + assertEquals(3, accumulator.size()); + Iterator it = accumulator.iterator(); + assertMatch(1, "bob", it.next()); + assertMatch(7, "bob", it.next()); + assertMatch(18, "bob", it.next()); + } + + @Test + public void testFindAllMultipleRanges() { + bytes = new TestByteSource(addr(0), "xbobxxxx"); + bytes.addBytes(addr(100), "bobxxxxxx"); + bytes.addBytes(addr(200), "xxxxxx"); + bytes.addBytes(addr(300), "xxxx xxbo bxxx bob"); + MemorySearcher searcher = + new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT, TINY_CHUNK_SIZE); + searcher.findAll(accumulator, monitor); + assertEquals(4, accumulator.size()); + Iterator it = accumulator.iterator(); + assertMatch(1, "bob", it.next()); + assertMatch(100, "bob", it.next()); + assertMatch(306, "bob", it.next()); + assertMatch(312, "bob", it.next()); + } + + @Test + public void testNextWithFilter() { + bytes = new TestByteSource(addr(0), "xxbobxxxbob"); + MemorySearcher searcher = new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT); + searcher.setMatchFilter(r -> r.getAddress().getOffset() != 2); + + MemoryMatch match = searcher.findNext(addr(0), monitor); + assertMatch(8, "bob", match); + + } + + @Test + public void testPreviousWithFilter() { + bytes = new TestByteSource(addr(0), "xxbobxxxbob"); + MemorySearcher searcher = new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT); + searcher.setMatchFilter(r -> r.getAddress().getOffset() != 8); + + MemoryMatch match = searcher.findNext(addr(0), monitor); + assertMatch(2, "bob", match); + + } + + @Test + public void testAllWithFilter() { + bytes = new TestByteSource(addr(0), "bobx xxbo bxxx xxxx xbob xxxx bobx"); + MemorySearcher searcher = new MemorySearcher(bytes, bobMatcher, addrs(), SEARCH_LIMIT); + searcher.setMatchFilter(r -> r.getAddress().getOffset() % 2 == 0); // only even addresses + + searcher.findAll(accumulator, monitor); + + assertEquals(3, accumulator.size()); + Iterator it = accumulator.iterator(); + assertMatch(0, "bob", it.next()); + assertMatch(6, "bob", it.next()); + assertMatch(24, "bob", it.next()); + } + + private AddressSet addrs() { + return bytes.getAddressSet(); + } + + private void assertMatch(int address, String matchString, MemoryMatch match) { + assertNotNull(match); + assertEquals(addr(address), match.getAddress()); + assertEquals(matchString.length(), match.getLength()); + assertEqualBytes(bytes(matchString), match.getBytes()); + } + + private void assertEqualBytes(byte[] bytes1, byte[] bytes2) { + assertEquals(bytes1.length, bytes2.length); + for (int i = 0; i < bytes1.length; i++) { + assertEquals(bytes1[i], bytes2[i]); + } + } + + private byte[] bytes(String string) { + // remove spaces as they are there for formatting purposes + string = string.replaceAll(" ", ""); + return string.getBytes(); + } + + private class TestByteSource implements AddressableByteSource { + private AddressSet set = new AddressSet(); + private Map map = new HashMap<>(); + + TestByteSource(Address address, String data) { + addBytes(address, data); + } + + public AddressSet getAddressSet() { + return set; + } + + void addBytes(Address address, String data) { + byte[] dataBytes = bytes(data); + Address end = address.add(dataBytes.length - 1); + int beforeNumAddressRanges = set.getNumAddressRanges(); + set.addRange(address, end); + int afterNumAddressRanges = set.getNumAddressRanges(); + // this simplistic test implementation can't handle ranges that coalesce so make + // sure our address set has an addition range in case we mess up writing a test + assertEquals(beforeNumAddressRanges + 1, afterNumAddressRanges); + map.put(address, dataBytes); + } + + @Override + public int getBytes(Address address, byte[] byteData, int length) { + AddressRange range = set.getRangeContaining(address); + if (range == null) { + return 0; + } + Address minAddress = range.getMinAddress(); + int index = (int) address.subtract(minAddress); + byte[] sourceBytes = map.get(minAddress); + System.arraycopy(sourceBytes, index, byteData, 0, length); + return length; + } + + @Override + public List getSearchableRegions() { + return null; + } + + @Override + public void invalidate() { + // ignore + } + } + + private Address addr(long offset) { + return space.getAddress(offset); + } +} diff --git a/Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/plugin/IngestTask.java b/Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/plugin/IngestTask.java index a3c69c4a00..c3a0eeb9c7 100644 --- a/Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/plugin/IngestTask.java +++ b/Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/plugin/IngestTask.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. @@ -50,7 +50,7 @@ public class IngestTask extends Task { DomainFolder folder, String libraryFamilyName, String libraryVersion, String libraryVariant, String languageId, File commonSymbolsFile, FidService fidService, FidPopulateResultReporter reporter) { - super(title); + super(title, true, false, false, false); this.fidFile = fidFile; this.libraryRecord = libraryRecord; this.folder = folder; diff --git a/Ghidra/Features/SourceCodeLookup/src/main/java/ghidra/app/plugin/core/scl/SourceCodeLookupPlugin.java b/Ghidra/Features/SourceCodeLookup/src/main/java/ghidra/app/plugin/core/scl/SourceCodeLookupPlugin.java index f42dbf2209..25cc3c8cdb 100644 --- a/Ghidra/Features/SourceCodeLookup/src/main/java/ghidra/app/plugin/core/scl/SourceCodeLookupPlugin.java +++ b/Ghidra/Features/SourceCodeLookup/src/main/java/ghidra/app/plugin/core/scl/SourceCodeLookupPlugin.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. @@ -15,12 +15,12 @@ */ package ghidra.app.plugin.core.scl; -import java.awt.event.KeyEvent; import java.io.*; import java.net.Socket; import docking.ActionContext; -import docking.action.*; +import docking.action.DockingAction; +import docking.action.MenuData; import docking.tool.ToolConstants; import ghidra.app.CorePluginPackage; import ghidra.app.context.ProgramLocationActionContext; @@ -94,7 +94,9 @@ public class SourceCodeLookupPlugin extends ProgramPlugin { // how to define the group/menu position. For now, just put the menu in the main menu bar. // lookupSourceCodeAction.setPopupMenuData(new MenuData(POPUP_PATH, null, "Label", // MenuData.NO_MNEMONIC, "z")); - lookupSourceCodeAction.setKeyBindingData(new KeyBindingData(KeyEvent.VK_F3, 0)); + + // F3 is conflicting with memory search again. + // lookupSourceCodeAction.setKeyBindingData(new KeyBindingData(KeyEvent.VK_F3, 0)); lookupSourceCodeAction.setHelpLocation( new HelpLocation("SourceCodeLookupPlugin", "Source_Code_Lookup_Plugin")); tool.addAction(lookupSourceCodeAction); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/ActionContext.java b/Ghidra/Framework/Docking/src/main/java/docking/ActionContext.java index 35846dce37..519a74f5f7 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/ActionContext.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/ActionContext.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. @@ -179,4 +179,12 @@ public interface ActionContext { */ public Component getSourceComponent(); + /** + * Sets the source component for this ActionContext. + * + * @param sourceComponent the source component + * @return this context + */ + public ActionContext setSourceComponent(Component sourceComponent); + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DefaultActionContext.java b/Ghidra/Framework/Docking/src/main/java/docking/DefaultActionContext.java index 2e45bb4052..1e5c7e6c66 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DefaultActionContext.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DefaultActionContext.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. @@ -164,6 +164,12 @@ public class DefaultActionContext implements ActionContext { return sourceComponent; } + @Override + public ActionContext setSourceComponent(Component sourceComponent) { + this.sourceComponent = sourceComponent; + return this; + } + @Override public String toString() { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockableHeader.java b/Ghidra/Framework/Docking/src/main/java/docking/DockableHeader.java index aed16982ec..d5d22b99a0 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockableHeader.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockableHeader.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. @@ -235,10 +235,14 @@ public class DockableHeader extends GenericHeader Component firstComponent = policy.getFirstComponent(dockComp); if (firstComponent == null) { ComponentPlaceholder info = dockComp.getComponentWindowingPlaceholder(); + String title = ""; + if (info != null) { + title = ": Title: " + info.getTitle() + ""; + } Msg.debug(this, - "Found a ComponentProvider that does not contain a " + "focusable component: " + - info.getTitle() + ". ComponentProviders are " + - "required to have at least one focusable component!"); + "Found a Component Provider that does not contain a focusable component" + + title + + ". Component Providers are required to have at least one focusable component!"); setSelected(false); // can't select it can't take focus } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/menu/ButtonState.java b/Ghidra/Framework/Docking/src/main/java/docking/menu/ButtonState.java new file mode 100644 index 0000000000..2746abbcee --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/menu/ButtonState.java @@ -0,0 +1,73 @@ +/* ### + * 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 docking.menu; + +/** + * Defines one "state" for a {@link MultiStateButton}. Each button state represents one choice from + * a drop-down list of choices on the button. Each state provides information on what the button + * text should be when it is the active state, the text in the drop-down for picking the state, text + * for a tooltip description, and finally client data that the client can use to store info for + * processing the action when that state is active. + * + * @param the type of the client data object. + */ +public class ButtonState { + private String buttonText; + private String menuText; + private String description; + private T clientData; + + /** + * Constructor + * @param buttonText the text to display as both the drop-down choice and the active button text + * @param description the tooltip for this state + * @param clientData the client data for this state + */ + public ButtonState(String buttonText, String description, T clientData) { + this(buttonText, buttonText, description, clientData); + } + + /** + * Constructor + * @param buttonText the text to display in the button when this state is active + * @param menuText the text to display in the drop-down list + * @param description the tooltip for this state + * @param clientData the client data for this state + */ + public ButtonState(String buttonText, String menuText, String description, T clientData) { + this.buttonText = buttonText; + this.menuText = menuText; + this.description = description; + this.clientData = clientData; + } + + public String getButtonText() { + return buttonText; + } + + public String getMenuText() { + return menuText; + } + + public String getDescription() { + return description; + } + + public T getClientData() { + return clientData; + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/menu/MultiStateButton.java b/Ghidra/Framework/Docking/src/main/java/docking/menu/MultiStateButton.java new file mode 100644 index 0000000000..a3c34907f2 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/menu/MultiStateButton.java @@ -0,0 +1,414 @@ +/* ### + * 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 docking.menu; + +import java.awt.*; +import java.awt.event.*; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.border.CompoundBorder; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; + +import docking.*; +import generic.theme.GThemeDefaults.Colors; +import generic.theme.GThemeDefaults.Colors.Messages; +import ghidra.util.Swing; +import resources.Icons; +import resources.ResourceManager; +import utility.function.Dummy; + +/** + * A button that has a drop-down list of choosable {@link ButtonState}s. When a state is selected, + * it changes the behavior of the action associated with the button. This code is based on code + * for the {@link MultipleActionDockingToolbarButton}. + * + * @param The type of the user data associated with the {@link ButtonState}s + */ +public class MultiStateButton extends JButton { + + private Icon arrowIcon; + private Icon disabledArrowIcon; + + private static int ARROW_WIDTH = 6; + private static int ARROW_HEIGHT = 3; + private static int ARROW_ICON_WIDTH = 20; + private static int ARROW_ICON_HEIGHT = 15; + + private PopupMouseListener popupListener; + private JPopupMenu popupMenu; + private Rectangle arrowButtonRegion; + private long popupLastClosedTime; + private List> buttonStates; + private ButtonState currentButtonState; + + private Consumer> stateChangedConsumer = Dummy.consumer(); + + public MultiStateButton(List> buttonStates) { + setButtonStates(buttonStates); + installMouseListeners(); + + arrowButtonRegion = createArrowRegion(); + addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + super.componentResized(e); + arrowButtonRegion = createArrowRegion(); + repaint(); + } + }); + addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_DOWN) { + Swing.runLater(() -> popupMenu = showPopup()); + e.consume(); + } + } + }); + } + + public void setButtonStates(List> buttonStates) { + this.buttonStates = buttonStates; + if (buttonStates.size() == 1) { + arrowIcon = Icons.EMPTY_ICON; + disabledArrowIcon = Icons.EMPTY_ICON; + setHorizontalAlignment(SwingConstants.CENTER); // center text if no drop-down menu + } + else { + arrowIcon = new ArrowIcon(); + disabledArrowIcon = ResourceManager.getDisabledIcon(arrowIcon); + setHorizontalAlignment(SwingConstants.LEFT); // align left if we have drop-down menu + } + setCurrentButtonState(buttonStates.get(0)); + arrowButtonRegion = createArrowRegion(); + } + + /** + * Sets a consumer to be called when the user changes the active {@link ButtonState}. + * @param consumer the consumer to be called when the button state changes + */ + public void setStateChangedListener(Consumer> consumer) { + this.stateChangedConsumer = consumer; + } + + /** + * Sets the active button state for this button. + * @param buttonState the button state to be made active + */ + public void setCurrentButtonState(ButtonState buttonState) { + if (!buttonStates.contains(buttonState)) { + throw new IllegalArgumentException("Attempted to set button state to unknown state"); + } + this.currentButtonState = buttonState; + setText(buttonState.getButtonText()); + String tooltip = buttonState.getDescription(); + + setToolTipText(tooltip); + getAccessibleContext().setAccessibleDescription(tooltip); + stateChangedConsumer.accept(buttonState); + } + + /** + * Sets the active button state to the state that is associated with the given client data. + * @param clientData the client data to make its associated button state the active state + */ + public void setSelectedStateByClientData(T clientData) { + for (ButtonState buttonState : buttonStates) { + if (Objects.equals(clientData, buttonState.getClientData())) { + setCurrentButtonState(buttonState); + } + } + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + int y = (getHeight() - arrowIcon.getIconHeight()) / 2; + Icon icon = isEnabled() ? arrowIcon : disabledArrowIcon; + icon.paintIcon(this, g, arrowButtonRegion.x, y); + } + + @Override + public Dimension getPreferredSize() { + Dimension d = super.getPreferredSize(); + d.width = d.width + arrowIcon.getIconWidth(); + d.height = Math.max(d.height, arrowIcon.getIconHeight()); + return d; + } + + private Rectangle createArrowRegion() { + if (buttonStates.size() == 1) { + return new Rectangle(0, 0, 0, 0); + } + Dimension size = getSize(); + Border border = getBorder(); + + // Depending on the theme, the button may have thick borders to compensate for the extra + // space we requested in the preferred size method. Some themes do this via a compound + // border and using the outside border's right inset works very well to move the icon + // inside the border. + // Otherwise, we just use 3 as a decent compromise. Nimbus looks best with 3, but flat + // themes look best with 2 as they have a thinner "outside" border + int rightMargin = 3; + if (border instanceof CompoundBorder compoundBorder) { + Border outsideBorder = compoundBorder.getOutsideBorder(); + Insets borderInsets = outsideBorder.getBorderInsets(this); + rightMargin = borderInsets.right; + } + int w = arrowIcon.getIconWidth() + rightMargin; + int h = size.height; + return new Rectangle(size.width - w, 0, w, h); + } + + @Override + public void updateUI() { + + removeMouseListener(popupListener); + + super.updateUI(); + + installMouseListeners(); + } + + private void installMouseListeners() { + MouseListener[] mouseListeners = getMouseListeners(); + for (MouseListener mouseListener : mouseListeners) { + removeMouseListener(mouseListener); + } + + popupListener = new PopupMouseListener(mouseListeners); + addMouseListener(popupListener); + } + + protected ActionContext getActionContext() { + ComponentProvider provider = getComponentProvider(); + ActionContext context = provider == null ? null : provider.getActionContext(null); + final ActionContext actionContext = context == null ? new DefaultActionContext() : context; + return actionContext; + } + + private ComponentProvider getComponentProvider() { + DockingWindowManager manager = DockingWindowManager.getActiveInstance(); + if (manager == null) { + return null; + } + return manager.getActiveComponentProvider(); + } + + /** + * Show a popup containing all the actions below the button + * + * @return the popup menu that was shown + */ + protected JPopupMenu showPopup() { + + if (popupIsShowing()) { + popupMenu.setVisible(false); + return null; + } + + // + // showPopup() will handled 2 cases when this action's button is clicked: + // 1) show a popup if it was not showing + // 2) hide the popup if it was showing + // + // Case 2 requires timestamps. Java will close the popup as the button is clicked. This + // means that when we are told to show the popup as the result of a click, the popup will + // never be showing. To work around this, we track the elapsed time since last click. If + // the period is too short, then we assume Java closed the popup when the click happened + // and thus we should ignore it. + // + long elapsedTime = System.currentTimeMillis() - popupLastClosedTime; + if (elapsedTime < 500) { // somewhat arbitrary time window + return null; + } + + JPopupMenu menu = doCreateMenu(); + + menu.addPopupMenuListener(popupListener); + Point p = getPopupPoint(); + menu.show(this, p.x, p.y); + return menu; + } + + protected JPopupMenu doCreateMenu() { + + JPopupMenu menu = new JPopupMenu(); + ButtonGroup buttonGroup = new ButtonGroup(); + for (ButtonState state : buttonStates) { + + JCheckBoxMenuItem item = new JCheckBoxMenuItem(state.getMenuText()); + item.setToolTipText(state.getDescription()); + item.getAccessibleContext().setAccessibleDescription(state.getDescription()); + item.setSelected(state == currentButtonState); + buttonGroup.add(item); + + // a UI that handles alignment issues and allows for tabulating presentation + item.setUI(DockingMenuItemUI.createUI(item)); + item.addActionListener(e -> { + setCurrentButtonState(state); + }); + + menu.add(item); + } + return menu; + } + + public Point getPopupPoint() { + Rectangle bounds = getBounds(); + return new Point(bounds.width - arrowIcon.getIconWidth(), bounds.y + bounds.height); + } + + private boolean popupIsShowing() { + return (popupMenu != null) && popupMenu.isVisible(); + } + +//================================================================================================== +// Inner Classes +//================================================================================================== + private class ArrowIcon implements Icon { + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + g.setColor(Messages.HINT); + g.drawLine(x, y, x, y + ARROW_ICON_HEIGHT); + g.setColor(Colors.FOREGROUND); + + int arrowMiddleX = x + ARROW_ICON_WIDTH / 2; + int arrowStartX = arrowMiddleX - ARROW_WIDTH / 2; + int arrowEndX = arrowStartX + ARROW_WIDTH; + + int arrowStartY = y + ARROW_ICON_HEIGHT / 2 - ARROW_HEIGHT / 2; + int arrowEndY = arrowStartY; + int arrowMiddleY = arrowStartY + ARROW_HEIGHT; + + int[] xPoints = { arrowStartX, arrowEndX, arrowMiddleX }; + int[] yPoints = { arrowStartY, arrowEndY, arrowMiddleY }; + + Graphics2D graphics2D = (Graphics2D) g; + graphics2D.drawPolygon(xPoints, yPoints, 3); + graphics2D.fillPolygon(xPoints, yPoints, 3); + } + + @Override + public int getIconWidth() { + return ARROW_ICON_WIDTH; + } + + @Override + public int getIconHeight() { + return ARROW_ICON_HEIGHT; + } + } + + private class PopupMouseListener extends MouseAdapter implements PopupMenuListener { + private final MouseListener[] parentListeners; + + public PopupMouseListener(MouseListener[] parentListeners) { + this.parentListeners = parentListeners; + } + + @Override + public void mousePressed(MouseEvent e) { + + Point clickPoint = e.getPoint(); + if (isEnabled() && arrowButtonRegion.contains(clickPoint)) { + + // Unusual Code Alert: we need to put this call in an invoke later, since Java + // will update the focused window after we click. We need the focus to be + // correct before we show, since our menu is built with actions based upon the + // focused component. + Swing.runLater(() -> popupMenu = showPopup()); + + e.consume(); + model.setPressed(false); + model.setArmed(false); + model.setRollover(false); + return; + } + + for (MouseListener listener : parentListeners) { + listener.mousePressed(e); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + if (popupIsShowing()) { + e.consume(); + return; + } + + for (MouseListener listener : parentListeners) { + listener.mouseClicked(e); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + if (popupIsShowing()) { + e.consume(); + return; + } + + for (MouseListener listener : parentListeners) { + listener.mouseReleased(e); + } + } + + @Override + public void mouseEntered(MouseEvent e) { + if (popupIsShowing()) { + return; + } + + for (MouseListener listener : parentListeners) { + listener.mouseEntered(e); + } + } + + @Override + public void mouseExited(MouseEvent e) { + if (popupIsShowing()) { + return; + } + for (MouseListener listener : parentListeners) { + listener.mouseExited(e); + } + } + + @Override + public void popupMenuCanceled(PopupMenuEvent e) { + // no-op + } + + @Override + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + popupLastClosedTime = System.currentTimeMillis(); + } + + @Override + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { + // no-op + } + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java index 68f2f4aa69..667bfa7545 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PopupWindow.java @@ -18,8 +18,7 @@ package docking.widgets; import java.awt.*; import java.awt.event.*; import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Iterator; +import java.util.*; import java.util.List; import javax.swing.*; @@ -31,6 +30,7 @@ import generic.theme.GThemeDefaults.Colors.Palette; import generic.util.WindowUtilities; import ghidra.util.bean.GGlassPane; import ghidra.util.bean.GGlassPanePainter; +import util.CollectionUtils; /** * A generic window intended to be used as a temporary window to show information. This window is @@ -52,8 +52,7 @@ public class PopupWindow { } private static final PopupWindowPlacer DEFAULT_WINDOW_PLACER = - new PopupWindowPlacerBuilder() - .rightEdge(Location.BOTTOM) + new PopupWindowPlacerBuilder().rightEdge(Location.BOTTOM) .leftEdge(Location.BOTTOM) .bottomEdge(Location.RIGHT) .topEdge(Location.CENTER) @@ -61,7 +60,10 @@ public class PopupWindow { .throwsAssertException() .build(); - /** Area where user can mouse without hiding the window (in screen coordinates) */ + /** + * Area where user can mouse without hiding the window (in screen coordinates). A.K.A., the + * mouse neutral zone. + */ private Rectangle mouseMovementArea; private JWindow popup; private Component sourceComponent; @@ -237,9 +239,39 @@ public class PopupWindow { popupWindowPlacer == null ? DEFAULT_WINDOW_PLACER : popupWindowPlacer; } - public void showOffsetPopup(MouseEvent e, Rectangle keepVisibleSize, boolean forceShow) { + public void showOffsetPopup(MouseEvent e, Rectangle keepVisibleArea, boolean forceShow) { if (forceShow || DockingUtils.isTipWindowEnabled()) { - doShowPopup(e, keepVisibleSize, popupWindowPlacer); + PopupSource popupSource = new PopupSource(e, keepVisibleArea); + doShowPopup(popupSource); + } + } + + /** + * Shows this popup window unless popups are disabled as reported by + * {@link DockingUtils#isTipWindowEnabled()}. If {@code forceShow} is true, then the popup + * will be shown regardless of the state returned by {@link DockingUtils#isTipWindowEnabled()}. + * @param e the event + * @param forceShow true to show the popup even popups are disabled application-wide + */ + public void showPopup(MouseEvent e, boolean forceShow) { + if (forceShow || DockingUtils.isTipWindowEnabled()) { + PopupSource popupSource = new PopupSource(e); + doShowPopup(popupSource); + } + } + + /** + * Shows this popup window unless popups are disabled as reported by + * {@link DockingUtils#isTipWindowEnabled()}. If {@code forceShow} is true, then the popup + * will be shown regardless of the state returned by {@link DockingUtils#isTipWindowEnabled()}. + * @param component the component for the popup + * @param location the location to show the popup + * @param forceShow true to show the popup even popups are disabled application-wide + */ + public void showPopup(Component component, Point location, boolean forceShow) { + if (forceShow || DockingUtils.isTipWindowEnabled()) { + PopupSource popupSource = new PopupSource(component, location, null); + doShowPopup(popupSource); } } @@ -253,34 +285,32 @@ public class PopupWindow { } /** - * Shows this popup window unless popups are disabled as reported by - * {@link DockingUtils#isTipWindowEnabled()}. If {@code forceShow} is true, then the popup - * will be shown regardless of the state returned by {@link DockingUtils#isTipWindowEnabled()}. - * @param e the event - * @param forceShow true to show the popup even popups are disabled application-wide + * Shows the popup window. This will hide any existing popup windows, adjusts the new popup + * to avoid covering the keep visible area and then shows the popup. + * + * @param popupSource the popup source that contains info about the source of the popup, such + * as the component, a mouse event and any area to keep visible. */ - public void showPopup(MouseEvent e, boolean forceShow) { - if (forceShow || DockingUtils.isTipWindowEnabled()) { - doShowPopup(e, null, popupWindowPlacer); - } - } + private void doShowPopup(PopupSource popupSource) { - private void doShowPopup(MouseEvent e, Rectangle keepVisibleSize, PopupWindowPlacer placer) { hideAllWindows(); - sourceComponent = e.getComponent(); + sourceComponent = popupSource.getSource(); sourceComponent.addMouseListener(sourceMouseListener); sourceComponent.addMouseMotionListener(sourceMouseMotionListener); - Dimension popupDimension = popup.getSize(); - ensureSize(popupDimension); + Dimension popupSize = popup.getSize(); + ensureSize(popupSize); - Rectangle keepVisibleArea = createKeepVisibleArea(e, keepVisibleSize); + // + // Creates a rectangle that contains both given rectangles entirely and includes padding. + // The padding allows users to mouse over the edge of the hovered area without triggering + // the popup to close. + // + Rectangle visibleArea = popupSource.getScreenKeepVisibleArea(); Rectangle screenBounds = WindowUtilities.getVisibleScreenBounds().getBounds(); - Rectangle placement = placer.getPlacement(popupDimension, keepVisibleArea, screenBounds); - mouseMovementArea = createMovementArea(placement, keepVisibleArea); - - installDebugPainter(e); + Rectangle placement = popupWindowPlacer.getPlacement(popupSize, visibleArea, screenBounds); + mouseMovementArea = placement.union(visibleArea); popup.setBounds(placement); popup.setVisible(true); @@ -290,26 +320,7 @@ public class PopupWindow { VISIBLE_POPUPS.add(new WeakReference<>(this)); } - private Rectangle createKeepVisibleArea(MouseEvent e, Rectangle keepVisibleAea) { - - Rectangle newArea; - if (keepVisibleAea == null) { - Point point = new Point(e.getPoint()); - newArea = new Rectangle(point); - newArea.grow(X_PADDING, Y_PADDING); // pad to avoid placing the popup too close - } - else { - newArea = new Rectangle(keepVisibleAea); - } - - Point point = newArea.getLocation(); - SwingUtilities.convertPointToScreen(point, sourceComponent); - newArea.setLocation(point); - - return newArea; - } - - private void ensureSize(Dimension popupDimension) { + private static void ensureSize(Dimension popupDimension) { Dimension screenDimension = WindowUtilities.getVisibleScreenBounds().getBounds().getSize(); if (screenDimension.width < popupDimension.width) { @@ -321,67 +332,120 @@ public class PopupWindow { } } - /** - * Creates a rectangle that contains both given rectangles entirely and includes padding. - * The padding allows users to mouse over the edge of the hovered area without triggering the - * popup to close. - */ - private Rectangle createMovementArea(Rectangle popupBounds, Rectangle hoverRectangle) { - Rectangle result = popupBounds.union(hoverRectangle); - return result; - } - - private void installDebugPainter(MouseEvent e) { -// GGlassPane glassPane = GGlassPane.getGlassPane(sourceComponent); -// ShapeDebugPainter painter = new ShapeDebugPainter(e, null, neutralMotionZone); -// painters.forEach(p -> glassPane.removePainter(p)); -// -// glassPane.addPainter(painter); -// painters.add(painter); - } - //================================================================================================== // Inner Classes //================================================================================================== - // for debug -// private static List painters = new ArrayList<>(); + /** + * A class that holds info related to the source of a hover request. This is used to position + * the popup window that will be shown. + */ + private class PopupSource { - /** Paints shapes used by this class (useful for debugging) */ - @SuppressWarnings("unused") - // enabled as needed - private class ShapeDebugPainter implements GGlassPanePainter { + private Component source; + private Rectangle screenKeepVisibleArea; + private Point location; - private MouseEvent sourceEvent; - private Rectangle bounds; - - ShapeDebugPainter(MouseEvent sourceEvent, Rectangle bounds) { - this.sourceEvent = sourceEvent; - this.bounds = bounds; + PopupSource(MouseEvent e) { + this(e, null); } - @Override - public void paint(GGlassPane glassPane, Graphics g) { + PopupSource(MouseEvent e, Rectangle keepVisibleArea) { + this(e.getComponent(), e.getPoint(), keepVisibleArea); + } - // bounds of the popup and the mouse neutral zone - if (bounds != null) { - Rectangle r = bounds; - Point p = new Point(r.getLocation()); - SwingUtilities.convertPointFromScreen(p, glassPane); + PopupSource(Component source, Point location, Rectangle keepVisibleArea) { - Color c = Palette.LAVENDER; - g.setColor(c); - g.fillRect(p.x, p.y, r.width, r.height); + if (CollectionUtils.isAllNull(location, keepVisibleArea)) { + throw new NullPointerException("Both location and keepVisibleArea cannot be null"); + } + if (keepVisibleArea == null) { + keepVisibleArea = new Rectangle(location, new Dimension(0, 0)); + } + this.location = location; + this.source = source; + this.screenKeepVisibleArea = createScreenKeepVisibleArea(location, keepVisibleArea); + + installDebugPainter(keepVisibleArea); + } + + Component getSource() { + return source; + } + + Rectangle getScreenKeepVisibleArea() { + return screenKeepVisibleArea; + } + + private Rectangle createScreenKeepVisibleArea(Point p, Rectangle keepVisibleAea) { + + Rectangle newArea = keepVisibleAea; + if (keepVisibleAea == null) { + Point point = new Point(p); + newArea = new Rectangle(point); + newArea.grow(X_PADDING, Y_PADDING); // pad to avoid placing the popup too close } - // show where the user hovered - if (sourceEvent != null) { - Point p = sourceEvent.getPoint(); - p = SwingUtilities.convertPoint(sourceEvent.getComponent(), p.x, p.y, glassPane); - g.setColor(Palette.RED); - int offset = 10; - g.fillRect(p.x - offset, p.y - offset, (offset * 2), (offset * 2)); + return createScreenKeepVisibleArea(newArea); + } + + private Rectangle createScreenKeepVisibleArea(Rectangle keepVisibleAea) { + + Objects.requireNonNull(keepVisibleAea); + + Rectangle newArea = new Rectangle(keepVisibleAea); + Point point = newArea.getLocation(); + SwingUtilities.convertPointToScreen(point, source.getParent()); + newArea.setLocation(point); + return newArea; + } + + // for debug + private void installDebugPainter(Rectangle keepVisibleArea) { +// +// GGlassPane glassPane = GGlassPane.getGlassPane(source); +// for (GGlassPanePainter p : painters) { +// glassPane.removePainter(p); +// } +// ShapeDebugPainter painter = new ShapeDebugPainter(); +// +// glassPane.addPainter(painter); +// painters.add(painter); + } + + @SuppressWarnings("unused") + private static List painters = new ArrayList<>(); + + /** Paints shapes used by this class (useful for debugging) */ + @SuppressWarnings("unused") // enabled as needed + private class ShapeDebugPainter implements GGlassPanePainter { + + @Override + public void paint(GGlassPane glassPane, Graphics g) { + + int alpha = 150; + + // bounds of the popup and the mouse neutral zone + if (mouseMovementArea != null) { + Rectangle r = mouseMovementArea; + Point p = new Point(r.getLocation()); + SwingUtilities.convertPointFromScreen(p, glassPane); + + Color c = Palette.LAVENDER.withAlpha(alpha); + g.setColor(c); + g.fillRect(p.x, p.y, r.width, r.height); + } + + // show where the user hovered + if (location != null) { + Point p = new Point(location); + p = SwingUtilities.convertPoint(source.getParent(), p.x, p.y, glassPane); + g.setColor(Palette.RED.withAlpha(alpha)); + int offset = 10; + g.fillRect(p.x - offset, p.y - offset, (offset * 2), (offset * 2)); + } } } } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/combobox/GhidraComboBox.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/combobox/GhidraComboBox.java index 5c9ff98269..c7d995207e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/combobox/GhidraComboBox.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/combobox/GhidraComboBox.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. @@ -38,7 +38,7 @@ import ghidra.util.exception.AssertException; * *

* 2) Adds the auto-completion feature. As a user types in the field, the combo box suggest the - * nearest matching entry in the combo box model. + * nearest matching entry in the combo box model. This is enabled by default. * *

* It also fixes the following bug: @@ -70,6 +70,7 @@ public class GhidraComboBox extends JComboBox implements GComponent { private PassThroughActionListener passThroughActionListener; private PassThroughKeyListener passThroughKeyListener; private PassThroughDocumentListener passThroughDocumentListener; + private DocumentListener documentListener; /** * Default constructor. @@ -344,6 +345,21 @@ public class GhidraComboBox extends JComboBox implements GComponent { else { super.requestFocus(); } + + } + + /** + * This enables or disables auto completion. When on, the combobox will attempt to auto-fill + * the input text box with drop-down items that start with the text entered. This behavior + * may not be desirable when the drop-down list is more than just a list of previously typed + * strings. Auto completion is on by default. + * @param enable if true, auto completion is on, otherwise it is off. + */ + public void setAutoCompleteEnabled(boolean enable) { + removeDocumentListener(documentListener); + if (enable) { + addDocumentListener(documentListener); + } } private String matchHistory(String input) { @@ -397,8 +413,8 @@ public class GhidraComboBox extends JComboBox implements GComponent { if (getRenderer() instanceof JComponent) { GComponent.setHTMLRenderingFlag((JComponent) getRenderer(), false); } - // add our internal listener to with all the others that the pass through listener will call - addDocumentListener(new MatchingItemsDocumentListener()); + documentListener = new MatchingItemsDocumentListener(); + addDocumentListener(documentListener); } @@ -422,7 +438,7 @@ public class GhidraComboBox extends JComboBox implements GComponent { textField.getDocument().addDocumentListener(passThroughDocumentListener); } - private JTextField getTextField() { + public JTextField getTextField() { Object object = getEditor().getEditorComponent(); if (object instanceof JTextField textField) { return textField; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/IncrementalLoadJob.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/IncrementalLoadJob.java index d872eb70f4..371dfc829e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/IncrementalLoadJob.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/IncrementalLoadJob.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. @@ -58,7 +58,7 @@ public class IncrementalLoadJob extends Job implements ThreadedTable this.incrementalAccumulator = new IncrementalUpdatingAccumulator(); notifyStarted(monitor); - + boolean error = false; try { doExecute(monitor); } @@ -70,13 +70,14 @@ public class IncrementalLoadJob extends Job implements ThreadedTable // console. Plus, handling it here gives us a chance to notify that the process is // complete. String name = threadedModel.getName(); + error = true; Msg.showError(this, null, "Unexpected Exception", "Unexpected exception loading table model \"" + name + "\"", e); } } boolean interrupted = Thread.currentThread().isInterrupted(); - notifyCompleted(hasBeenCancelled(monitor) || interrupted); + notifyCompleted(hasBeenCancelled(monitor) || interrupted || error); // all data should have been posted at this point; clean up any data left in the accumulator incrementalAccumulator.clear(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/ThreadedTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/ThreadedTableModel.java index 8e226fb3fc..61756b8eff 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/ThreadedTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/threaded/ThreadedTableModel.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. @@ -18,6 +18,7 @@ package docking.widgets.table.threaded; import static docking.widgets.table.AddRemoveListItem.Type.*; import java.util.*; +import java.util.function.Consumer; import javax.swing.SwingUtilities; import javax.swing.event.TableModelEvent; @@ -860,6 +861,17 @@ public abstract class ThreadedTableModel listeners.add(new OneTimeListenerWrapper(listener)); } + /** + * Adds a consumer that will be notified when the model finishes loading. The consumer + * is passed a boolean that indicates is true if the loading was cancelled. After the + * table completes loading, the listener is removed. + * + * @param completedLoadingConsumer the consumer to be notified when the table is done loading + */ + public void addInitialLoadListener(Consumer completedLoadingConsumer) { + listeners.add(new OneTimeCompletedLoadingAdapter(completedLoadingConsumer)); + } + /** * This is a way to know about updates from the table. * @@ -1016,4 +1028,34 @@ public abstract class ThreadedTableModel } } + /** + * Class to adapt a {@link ThreadedTableModelListener} to a single use Consumer that gets + * notified once when the table is done loading and then removes the threaded table model + * listener. + */ + private class OneTimeCompletedLoadingAdapter implements ThreadedTableModelListener { + private Consumer completedLoadingConsumer; + + OneTimeCompletedLoadingAdapter(Consumer completedLoadingConsumer) { + this.completedLoadingConsumer = completedLoadingConsumer; + } + + @Override + public void loadPending() { + // do nothing + } + + @Override + public void loadingStarted() { + // do nothing + } + + @Override + public void loadingFinished(boolean wasCancelled) { + removeThreadedTableModelListener(this); + completedLoadingConsumer.accept(wasCancelled); + } + + } + } diff --git a/Ghidra/Framework/Gui/src/main/java/ghidra/util/task/Task.java b/Ghidra/Framework/Gui/src/main/java/ghidra/util/task/Task.java index 5331e73fef..fed6ddd599 100644 --- a/Ghidra/Framework/Gui/src/main/java/ghidra/util/task/Task.java +++ b/Ghidra/Framework/Gui/src/main/java/ghidra/util/task/Task.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. @@ -44,7 +44,7 @@ public abstract class Task implements MonitoredRunnable { * @param title the title associated with the task */ public Task(String title) { - this(title, true, false, false, false); + this(title, true, false, true, false); } /** diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/address/AddressRangeSplitter.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/address/AddressRangeSplitter.java new file mode 100644 index 0000000000..7c46182aed --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/address/AddressRangeSplitter.java @@ -0,0 +1,85 @@ +/* ### + * 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.model.address; + +/** + * {@link AddressRangeIterator} that takes a single address range and breaks it down into smaller + * address ranges of a specified maximum size. This is useful for clients that want to break + * down the processing of large address ranges into manageable chunks. For example, searching the + * bytes in memory can be broken so that chunks can be read into reasonably sized buffers. + */ +public class AddressRangeSplitter implements AddressRangeIterator { + private AddressRange remainingRange; + private int splitSize; + private boolean forward; + + /** + * Constructor + * @param range the address range to split apart + * @param splitSize the max size of each sub range + * @param forward if true, the sub ranges will be returned in address order; otherwise they + * will be returned in reverse address order. + */ + public AddressRangeSplitter(AddressRange range, int splitSize, boolean forward) { + remainingRange = range; + this.splitSize = splitSize; + this.forward = forward; + } + + @Override + public boolean hasNext() { + return remainingRange != null; + } + + @Override + public AddressRange next() { + if (remainingRange == null) { + return null; + } + if (isRangeSmallEnough()) { + AddressRange returnValue = remainingRange; + remainingRange = null; + return returnValue; + } + return forward ? extractChunkFromStart() : extractChunkFromEnd(); + } + + private AddressRange extractChunkFromStart() { + Address start = remainingRange.getMinAddress(); + Address end = start.add(splitSize - 1); + remainingRange = new AddressRangeImpl(end.next(), remainingRange.getMaxAddress()); + return new AddressRangeImpl(start, end); + } + + private AddressRange extractChunkFromEnd() { + Address end = remainingRange.getMaxAddress(); + Address start = end.subtract(splitSize - 1); + + remainingRange = new AddressRangeImpl(remainingRange.getMinAddress(), start.previous()); + return new AddressRangeImpl(start, end); + } + + private boolean isRangeSmallEnough() { + try { + int size = remainingRange.getBigLength().intValueExact(); + return size <= splitSize; + } + catch (ArithmeticException e) { + return false; + } + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/address/AddressSetView.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/address/AddressSetView.java index 218ae9ed91..a4a6f08270 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/address/AddressSetView.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/address/AddressSetView.java @@ -253,6 +253,26 @@ public interface AddressSetView extends Iterable { */ public Address findFirstAddressInCommon(AddressSetView set); + /** + * Returns the number of address in this address set before the given address. + * @param address the address after the last address to be counted + * @return the number of address in this address set before the given address + */ + public default long getAddressCountBefore(Address address) { + long count = 0; + for (AddressRange range : getAddressRanges()) { + if (range.getMinAddress().compareTo(address) > 0) { + return count; + } + else if (range.contains(address)) { + count += address.subtract(range.getMinAddress()); + return count; + } + count += range.getLength(); + } + return count; + } + /** * Trim address set removing all addresses less-than-or-equal to specified * address based upon {@link Address} comparison. @@ -305,4 +325,5 @@ public interface AddressSetView extends Iterable { } return trimmedSet; } + } diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/address/AddressRangeSplitterTest.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/address/AddressRangeSplitterTest.java new file mode 100644 index 0000000000..5329b48ec4 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/model/address/AddressRangeSplitterTest.java @@ -0,0 +1,128 @@ +/* ### + * 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.model.address; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; + +public class AddressRangeSplitterTest { + private AddressSpace space; + + @Before + public void setUp() { + space = new GenericAddressSpace("test", 64, AddressSpace.TYPE_RAM, 0); + } + + @Test + public void testRangeDoesntNeedSplitting() { + AddressRange range = range(0, 100); + AddressRangeSplitter splitter = new AddressRangeSplitter(range, 1000, true); + assertTrue(splitter.hasNext()); + assertEquals(range(0, 100), splitter.next()); + assertFalse(splitter.hasNext()); + assertNull(splitter.next()); + } + + @Test + public void testRangeSplitting() { + AddressRange range = range(0, 500); + AddressRangeSplitter splitter = new AddressRangeSplitter(range, 100, true); + + assertTrue(splitter.hasNext()); + assertEquals(range(0, 99), splitter.next()); + assertTrue(splitter.hasNext()); + assertEquals(range(100, 199), splitter.next()); + assertTrue(splitter.hasNext()); + assertEquals(range(200, 299), splitter.next()); + assertTrue(splitter.hasNext()); + assertEquals(range(300, 399), splitter.next()); + assertTrue(splitter.hasNext()); + assertEquals(range(400, 499), splitter.next()); + assertTrue(splitter.hasNext()); + assertEquals(range(500, 500), splitter.next()); + + assertFalse(splitter.hasNext()); + assertNull(splitter.next()); + } + + @Test + public void testSplittingRangeWhoseLengthIsLong() { + AddressRange range = range(0, 0xffffffffffffffffL); + AddressRangeSplitter splitter = new AddressRangeSplitter(range, 100, true); + + assertTrue(splitter.hasNext()); + assertEquals(range(0, 99), splitter.next()); + assertTrue(splitter.hasNext()); + assertEquals(range(100, 199), splitter.next()); + assertTrue(splitter.hasNext()); + + } + + @Test + public void testReverseRangeDoesntNeedSplitting() { + AddressRange range = range(0, 100); + AddressRangeSplitter splitter = new AddressRangeSplitter(range, 1000, true); + assertTrue(splitter.hasNext()); + assertEquals(range(0, 100), splitter.next()); + assertFalse(splitter.hasNext()); + assertNull(splitter.next()); + } + + @Test + public void testReverseRangeSplitting() { + AddressRange range = range(0, 500); + AddressRangeSplitter splitter = new AddressRangeSplitter(range, 100, false); + + assertTrue(splitter.hasNext()); + assertEquals(range(401, 500), splitter.next()); + assertTrue(splitter.hasNext()); + assertEquals(range(301, 400), splitter.next()); + assertTrue(splitter.hasNext()); + assertEquals(range(201, 300), splitter.next()); + assertTrue(splitter.hasNext()); + assertEquals(range(101, 200), splitter.next()); + assertTrue(splitter.hasNext()); + assertEquals(range(1, 100), splitter.next()); + assertTrue(splitter.hasNext()); + assertEquals(range(0, 0), splitter.next()); + + assertFalse(splitter.hasNext()); + assertNull(splitter.next()); + } + + @Test + public void testReverseSplittingRangeWhoseLengthIsLong() { + AddressRange range = range(0, 0xffffffffffffffffL); + AddressRangeSplitter splitter = new AddressRangeSplitter(range, 0x100, false); + + assertTrue(splitter.hasNext()); + assertEquals(range(0xffffffffffffff00L, 0xffffffffffffffffL), splitter.next()); + assertTrue(splitter.hasNext()); + assertEquals(range(0xfffffffffffffe00L, 0xfffffffffffffeffL), splitter.next()); + assertTrue(splitter.hasNext()); + + } + + private AddressRange range(long start, long end) { + return new AddressRangeImpl(addr(start), addr(end)); + } + + private Address addr(long offset) { + return space.getAddress(offset); + } +} diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/MemorySearchScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/MemorySearchScreenShots.java index c38c7cdf42..03cd066596 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/MemorySearchScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/MemorySearchScreenShots.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. @@ -17,17 +17,17 @@ package help.screenshot; import java.awt.*; -import javax.swing.*; - import org.junit.Before; import org.junit.Test; -import docking.DialogComponentProvider; import docking.action.DockingActionIf; import generic.theme.GThemeDefaults.Colors.Palette; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.app.plugin.core.codebrowser.CodeViewerProvider; -import ghidra.app.plugin.core.searchmem.mask.MnemonicSearchPlugin; +import ghidra.features.base.memsearch.format.SearchFormat; +import ghidra.features.base.memsearch.gui.MemorySearchProvider; +import ghidra.features.base.memsearch.gui.SearchSettings; +import ghidra.features.base.memsearch.mnemonic.MnemonicSearchPlugin; import ghidra.program.model.address.*; /** @@ -51,119 +51,46 @@ public class MemorySearchScreenShots extends AbstractSearchScreenShots { } @Test - public void testSearchMemoryHex() { - - moveTool(500, 500); - - performAction("Search Memory", "MemSearchPlugin", false); + public void testMemorySearchProvider() { + performAction("Memory Search", "MemorySearchPlugin", false); waitForSwing(); - DialogComponentProvider dialog = getDialog(); - JTextField textField = (JTextField) getInstanceField("valueField", dialog); - setText(textField, "12 34"); + MemorySearchProvider provider = getComponentProvider(MemorySearchProvider.class); - JToggleButton button = (JToggleButton) getInstanceField("advancedButton", dialog); - pressButton(button); + runSwing(() -> provider.setSearchInput("12 34")); - waitForSwing(); + captureIsolatedProvider(provider, 700, 400); - captureDialog(DialogComponentProvider.class); } @Test - public void testSearchMemoryRegex() { - - moveTool(500, 500); - - performAction("Search Memory", "MemSearchPlugin", false); + public void testMemorySearchProviderWithOptionsOn() { + performAction("Memory Search", "MemorySearchPlugin", false); waitForSwing(); - DialogComponentProvider dialog = getDialog(); - JRadioButton regexRadioButton = - (JRadioButton) findAbstractButtonByText(dialog.getComponent(), "Regular Expression"); - pressButton(regexRadioButton); + MemorySearchProvider provider = getComponentProvider(MemorySearchProvider.class); - JTextField textField = (JTextField) getInstanceField("valueField", dialog); - setText(textField, "\\x50.{0,10}\\x55"); + runSwing(() -> { + provider.setSearchInput("12 34"); + provider.showOptions(true); + }); - JToggleButton button = (JToggleButton) getInstanceField("advancedButton", dialog); - pressButton(button); - - waitForSwing(); - - captureDialog(DialogComponentProvider.class); + captureIsolatedProvider(provider, 700, 650); } @Test - public void testSearchMemoryBinary() { - - moveTool(500, 500); - - performAction("Search Memory", "MemSearchPlugin", false); + public void testMemorySearchProviderWithScanPanelOn() { + performAction("Memory Search", "MemorySearchPlugin", false); waitForSwing(); - DialogComponentProvider dialog = getDialog(); - JRadioButton binaryRadioButton = - (JRadioButton) findAbstractButtonByText(dialog.getComponent(), "Binary"); - pressButton(binaryRadioButton); + MemorySearchProvider provider = getComponentProvider(MemorySearchProvider.class); - JTextField textField = (JTextField) getInstanceField("valueField", dialog); - setText(textField, "10xx0011"); + runSwing(() -> { + provider.setSearchInput("12 34"); + provider.showScanPanel(true); + }); - JToggleButton button = (JToggleButton) getInstanceField("advancedButton", dialog); - pressButton(button); - - waitForSwing(); - - captureDialog(DialogComponentProvider.class); - } - - @Test - public void testSearchMemoryDecimal() { - - moveTool(500, 500); - - performAction("Search Memory", "MemSearchPlugin", false); - waitForSwing(); - - DialogComponentProvider dialog = getDialog(); - JRadioButton decimalRadioButton = - (JRadioButton) findAbstractButtonByText(dialog.getComponent(), "Decimal"); - pressButton(decimalRadioButton); - - JTextField textField = (JTextField) getInstanceField("valueField", dialog); - setText(textField, "1234"); - - JToggleButton button = (JToggleButton) getInstanceField("advancedButton", dialog); - pressButton(button); - - waitForSwing(); - - captureDialog(DialogComponentProvider.class); - } - - @Test - public void testSearchMemoryString() { - - moveTool(500, 500); - - performAction("Search Memory", "MemSearchPlugin", false); - waitForSwing(); - - DialogComponentProvider dialog = getDialog(); - JRadioButton stringRadioButton = - (JRadioButton) findAbstractButtonByText(dialog.getComponent(), "String"); - pressButton(stringRadioButton); - - JTextField textField = (JTextField) getInstanceField("valueField", dialog); - setText(textField, "Hello"); - - JToggleButton button = (JToggleButton) getInstanceField("advancedButton", dialog); - pressButton(button); - - waitForSwing(); - - captureDialog(DialogComponentProvider.class); + captureIsolatedProvider(provider, 700, 500); } @Test @@ -208,6 +135,21 @@ public class MemorySearchScreenShots extends AbstractSearchScreenShots { image = tf.getImage(); } + @Test + public void testSearchMemoryRegex() { + performAction("Memory Search", "MemorySearchPlugin", false); + waitForSwing(); + + MemorySearchProvider provider = getComponentProvider(MemorySearchProvider.class); + + runSwing(() -> { + provider.setSettings(new SearchSettings().withSearchFormat(SearchFormat.REG_EX)); + provider.setSearchInput("\\x50.{0,10}\\x55"); + }); + + captureIsolatedProvider(provider, 700, 300); + } + @Test public void testSearchInstructionsExcludeOperands() { Font font = new Font("Monospaced", Font.PLAIN, 14);