From 6fb115358a5247161521d24b1af856c93aec395b Mon Sep 17 00:00:00 2001 From: ghidragon <106987263+ghidragon@users.noreply.github.com> Date: Fri, 28 Feb 2025 20:08:40 -0500 Subject: [PATCH] GP-5310 Created global search and replace feature --- .../AbstractDBTraceProgramViewListing.java | 17 + .../program/DBTraceProgramViewFragment.java | 9 +- .../program/DBTraceProgramViewRootModule.java | 9 +- Ghidra/Features/Base/certification.manifest | 15 +- .../Base/data/ExtensionPoint.manifest | 1 + .../Base/data/base.icons.theme.properties | 6 +- .../Base/src/main/help/help/TOC_Source.xml | 13 +- .../topics/MemoryMapPlugin/Memory_Map.htm | 9 + .../help/topics/Search/SearchAndReplace.htm | 478 +++++++++++++++ .../help/help/topics/Search/Searching.htm | 11 +- .../Search/images/SearchAndReplaceDialog.png | Bin 0 -> 18914 bytes .../Search/images/SearchAndReplaceResults.png | Bin 0 -> 38164 bytes .../DefaultDataTypeManagerService.java | 11 +- .../core/comments/CommentsActionFactory.java | 9 +- .../plugin/core/comments/CommentsPlugin.java | 12 +- .../CompositeEditorProvider.java | 6 + .../StructureEditorProvider.java | 6 - .../core/datamgr/DataTypeManagerPlugin.java | 13 +- .../core/datamgr/DataTypesProvider.java | 39 ++ .../datamgr/editor/DataTypeEditorManager.java | 18 +- .../plugin/core/memory/MemoryMapPlugin.java | 9 +- .../plugin/core/memory/MemoryMapProvider.java | 32 +- .../searchtext/ListingDisplaySearcher.java | 8 +- .../core/symboltree/SymbolTreePlugin.java | 15 +- .../core/symboltree/SymbolTreeProvider.java | 5 + .../core/symboltree/SymbolTreeService.java | 29 + .../app/services/DataTypeManagerService.java | 21 +- .../app/services/ProgramTreeService.java | 23 +- .../base/quickfix/QuckFixTableProvider.java | 356 +++++++++++ .../features/base/quickfix/QuickFix.java | 231 ++++++++ .../base/quickfix/QuickFixStatus.java | 28 + .../base/quickfix/QuickFixStatusRenderer.java | 78 +++ .../base/quickfix/QuickFixTableModel.java | 346 +++++++++++ .../base/quickfix/TableDataLoader.java | 58 ++ .../features/base/replace/RenameQuickFix.java | 49 ++ .../base/replace/SearchAndReplaceDialog.java | 356 +++++++++++ .../base/replace/SearchAndReplaceHandler.java | 68 +++ .../base/replace/SearchAndReplacePlugin.java | 133 +++++ .../replace/SearchAndReplaceProvider.java | 106 ++++ .../SearchAndReplaceQuckFixTableLoader.java | 66 +++ .../base/replace/SearchAndReplaceQuery.java | 152 +++++ .../features/base/replace/SearchType.java | 93 +++ .../DataTypesSearchAndReplaceHandler.java | 274 +++++++++ ...tatypeCategorySearchAndReplaceHandler.java | 78 +++ ...istingCommentsSearchAndReplaceHandler.java | 75 +++ .../MemoryBlockSearchAndReplaceHandler.java | 64 ++ .../ProgramTreeSearchAndReplaceHandler.java | 100 ++++ .../SymbolsSearchAndReplaceHandler.java | 115 ++++ .../replace/items/CompositeFieldQuickFix.java | 116 ++++ .../replace/items/RenameCategoryQuickFix.java | 121 ++++ .../replace/items/RenameDataTypeQuickFix.java | 125 ++++ .../items/RenameEnumValueQuickFix.java | 127 ++++ .../replace/items/RenameFieldQuickFix.java | 89 +++ .../items/RenameMemoryBlockQuickFix.java | 79 +++ .../items/RenameProgramTreeGroupQuickFix.java | 144 +++++ .../replace/items/RenameSymbolQuickFix.java | 136 +++++ .../replace/items/UpdateCommentQuickFix.java | 96 +++ .../UpdateDataTypeDescriptionQuickFix.java | 128 ++++ .../items/UpdateEnumCommentQuickFix.java | 112 ++++ .../items/UpdateFieldCommentQuickFix.java | 82 +++ .../program/database/ProgramBuilder.java | 2 +- .../java/ghidra/util/table/GhidraTable.java | 2 +- .../table/actions/DeleteTableRowAction.java | 15 +- .../actions/MakeProgramSelectionAction.java | 28 +- .../TestDoubleDataTypeManagerService.java | 11 +- .../replace/AbstractSearchAndReplaceTest.java | 201 +++++++ .../CategoriesSearchAndReplaceTest.java | 95 +++ .../DataTypesSearchAndReplaceTest.java | 454 ++++++++++++++ .../ListingCommentsSearchAndReplaceTest.java | 194 ++++++ .../MemoryBlockSearchAndReplaceTest.java | 95 +++ .../ProgramTreeSearchAndReplaceTest.java | 176 ++++++ .../replace/SearchAndReplaceDialogTest.java | 200 +++++++ .../replace/SymbolsSearchAndReplaceTest.java | 553 ++++++++++++++++++ .../DecompilerCommentsActionFactory.java | 8 +- .../util/task/TaskMonitorComponent.java | 4 +- .../datastruct/AccumulatorSizeException.java | 30 + .../SizeRestrictedAccumulatorWrapper.java | 71 +++ Ghidra/Framework/Gui/certification.manifest | 1 + .../Framework/Gui/data/gui.theme.properties | 2 +- .../Gui/src/main/resources/images/tick.png | Bin 0 -> 537 bytes .../ghidra/program/database/ListingDB.java | 14 +- .../program/database/code/CodeManager.java | 42 +- .../program/database/module/FragmentDB.java | 9 +- .../program/database/module/ModuleDB.java | 9 +- .../program/model/listing/CodeUnit.java | 59 +- .../model/listing/CodeUnitComments.java | 40 ++ .../program/model/listing/CommentType.java | 28 + .../ghidra/program/model/listing/Group.java | 59 +- .../ghidra/program/model/listing/Listing.java | 80 ++- .../program/model/listing/StubListing.java | 14 +- ...CommentType.java => CommentTypeUtils.java} | 7 +- .../java/ghidra/util/UserSearchUtils.java | 10 +- .../SearchAndReplaceScreenShots.java | 92 +++ 93 files changed, 7469 insertions(+), 141 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/help/help/topics/Search/SearchAndReplace.htm create mode 100644 Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchAndReplaceDialog.png create mode 100644 Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchAndReplaceResults.png create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeService.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuckFixTableProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixStatus.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixStatusRenderer.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixTableModel.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/TableDataLoader.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/RenameQuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceDialog.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceHandler.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplacePlugin.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceQuckFixTableLoader.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceQuery.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchType.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/DataTypesSearchAndReplaceHandler.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/DatatypeCategorySearchAndReplaceHandler.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/ListingCommentsSearchAndReplaceHandler.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/MemoryBlockSearchAndReplaceHandler.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/ProgramTreeSearchAndReplaceHandler.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/SymbolsSearchAndReplaceHandler.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/CompositeFieldQuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameCategoryQuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameDataTypeQuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameEnumValueQuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameFieldQuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameMemoryBlockQuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameProgramTreeGroupQuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameSymbolQuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateCommentQuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateDataTypeDescriptionQuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateEnumCommentQuickFix.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateFieldCommentQuickFix.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/AbstractSearchAndReplaceTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/CategoriesSearchAndReplaceTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/DataTypesSearchAndReplaceTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/ListingCommentsSearchAndReplaceTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/MemoryBlockSearchAndReplaceTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/ProgramTreeSearchAndReplaceTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/SearchAndReplaceDialogTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/SymbolsSearchAndReplaceTest.java create mode 100644 Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/AccumulatorSizeException.java create mode 100644 Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/SizeRestrictedAccumulatorWrapper.java create mode 100644 Ghidra/Framework/Gui/src/main/resources/images/tick.png create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/CodeUnitComments.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/CommentType.java rename Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/{CommentType.java => CommentTypeUtils.java} (97%) create mode 100644 Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/SearchAndReplaceScreenShots.java diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewListing.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewListing.java index 8ffa102571..971c1fff3f 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewListing.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/AbstractDBTraceProgramViewListing.java @@ -439,6 +439,13 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV return getCommentAddresses(addrSet).getAddresses(forward); } + @Override + public long getCommentAddressCount() { + return program.viewport.unionedAddresses( + s -> program.trace.getCommentAdapter().getAddressSetView(Lifespan.at(s))) + .getNumAddresses(); + } + @Override public String getComment(int commentType, Address address) { try (LockHold hold = program.trace.lockRead()) { @@ -447,6 +454,16 @@ public abstract class AbstractDBTraceProgramViewListing implements TraceProgramV } } + @Override + public CodeUnitComments getAllComments(Address address) { + CommentType[] types = CommentType.values(); + String[] comments = new String[types.length]; + for (CommentType type : types) { + comments[type.ordinal()] = getComment(type, address); + } + return new CodeUnitComments(comments); + } + @Override public void setComment(Address address, int commentType, String comment) { program.trace.getCommentAdapter() diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewFragment.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewFragment.java index 6f5a197dc5..007097f7da 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewFragment.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewFragment.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. @@ -249,4 +249,9 @@ public class DBTraceProgramViewFragment implements ProgramFragment { public void move(Address min, Address max) throws NotFoundException { throw new UnsupportedOperationException(); } + + @Override + public boolean isDeleted() { + return false; + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRootModule.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRootModule.java index e77b345bca..bc8a4a705e 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRootModule.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/program/DBTraceProgramViewRootModule.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. @@ -239,4 +239,9 @@ public class DBTraceProgramViewRootModule implements ProgramModule { public long getTreeID() { return 0; } + + @Override + public boolean isDeleted() { + return false; + } } diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 5c4b89797b..615571e55b 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -516,7 +516,7 @@ src/main/help/help/topics/ScalarSearchPlugin/images/ScalarWindow.png||GHIDRA|||| 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/SearchAndReplace.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| @@ -533,6 +533,8 @@ src/main/help/help/topics/Search/images/MemorySearchProviderWithOptionsOn.png||G 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/SearchAndReplaceDialog.png||GHIDRA||||END| +src/main/help/help/topics/Search/images/SearchAndReplaceResults.png||GHIDRA||||END| src/main/help/help/topics/Search/images/SearchForAddressTables.png||GHIDRA||||END| src/main/help/help/topics/Search/images/SearchInstructionPatterns.png||GHIDRA||||END| src/main/help/help/topics/Search/images/SearchInstructionPatternsControlPanel.png||GHIDRA||||END| @@ -579,10 +581,6 @@ src/main/help/help/topics/SymbolTreePlugin/SymbolTree.htm||GHIDRA||||END| src/main/help/help/topics/SymbolTreePlugin/images/CreateExternalLocation.png||GHIDRA||||END| src/main/help/help/topics/SymbolTreePlugin/images/EditExternalLocation.png||GHIDRA||||END| src/main/help/help/topics/SymbolTreePlugin/images/SymbolTree.png||GHIDRA||||END| -src/main/help/help/topics/Tables/GhidraTableHeaders.html||GHIDRA||||END| -src/main/help/help/topics/Tables/images/BytesSettingsDialog.png||GHIDRA||reviewed||END| -src/main/help/help/topics/Tables/images/MultipleColumnSortDialog.png||GHIDRA||||END| -src/main/help/help/topics/Tables/images/SelectColumnsDialog.png||GHIDRA||||END| src/main/help/help/topics/Tool/Configure_Tool.htm||GHIDRA||||END| src/main/help/help/topics/Tool/Ghidra_Tool_Administration.htm||GHIDRA||||END| src/main/help/help/topics/Tool/ShowLog.htm||GHIDRA|||References wcbiema in screen snapshot|END| @@ -604,13 +602,6 @@ src/main/help/help/topics/Tool/images/SetToolAssociations.png||GHIDRA||||END| src/main/help/help/topics/Tool/images/ShowLog.png||GHIDRA||||END| src/main/help/help/topics/Tool/images/Tip.png||GHIDRA||||END| src/main/help/help/topics/TranslateStringsPlugin/TranslateStringsPlugin.htm||GHIDRA||||END| -src/main/help/help/topics/Trees/GhidraTreeFilter.html||GHIDRA||||END| -src/main/help/help/topics/Trees/images/Filter.png||GHIDRA||||END| -src/main/help/help/topics/Trees/images/FilterClearButton.png||GHIDRA||||END| -src/main/help/help/topics/Trees/images/FilterOptions.png||GHIDRA||||END| -src/main/help/help/topics/Trees/images/TableColumnFilter.png||GHIDRA||||END| -src/main/help/help/topics/Trees/images/TableColumnFilterAfterFilterApplied.png||GHIDRA||||END| -src/main/help/help/topics/Trees/images/TableColumnFilterDialog.png||GHIDRA||||END| src/main/help/help/topics/VSCodeIntegration/VSCodeIntegration.htm||GHIDRA||||END| src/main/help/help/topics/ValidateProgram/ValidateProgram.html||GHIDRA||||END| src/main/help/help/topics/ValidateProgram/images/ValidateProgram.png||GHIDRA||||END| diff --git a/Ghidra/Features/Base/data/ExtensionPoint.manifest b/Ghidra/Features/Base/data/ExtensionPoint.manifest index 6fada20354..c6e74d361d 100644 --- a/Ghidra/Features/Base/data/ExtensionPoint.manifest +++ b/Ghidra/Features/Base/data/ExtensionPoint.manifest @@ -22,3 +22,4 @@ OverviewColorService DWARFFunctionFixup ElfInfoProducer FSBFileHandler +SearchAndReplaceHandler diff --git a/Ghidra/Features/Base/data/base.icons.theme.properties b/Ghidra/Features/Base/data/base.icons.theme.properties index 6549fbe9b1..85b4e71297 100644 --- a/Ghidra/Features/Base/data/base.icons.theme.properties +++ b/Ghidra/Features/Base/data/base.icons.theme.properties @@ -204,7 +204,7 @@ icon.plugin.merge.conflict.lock = lock.gif icon.plugin.merge.conflict.unlock = unlock.gif icon.plugin.merge.status.pending = bullet_green.png icon.plugin.merge.status.in.progress = right.png -icon.plugin.merge.status.complete = checkmark_green.gif +icon.plugin.merge.status.complete = icon.checkmark.green icon.plugin.myprogramchanges.merge = vcMerge.png icon.plugin.myprogramchanges.checkin = vcCheckIn.png @@ -402,9 +402,7 @@ 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 - - - +icon.base.plugin.quickfix.done = icon.checkmark.green [Dark Defaults] diff --git a/Ghidra/Features/Base/src/main/help/help/TOC_Source.xml b/Ghidra/Features/Base/src/main/help/help/TOC_Source.xml index 506c93713f..0fbf5ed0d9 100644 --- a/Ghidra/Features/Base/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Features/Base/src/main/help/help/TOC_Source.xml @@ -327,12 +327,13 @@ - - - - - - + + + + + + + diff --git a/Ghidra/Features/Base/src/main/help/help/topics/MemoryMapPlugin/Memory_Map.htm b/Ghidra/Features/Base/src/main/help/help/topics/MemoryMapPlugin/Memory_Map.htm index 542cbfd2e2..a7fc8a93aa 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/MemoryMapPlugin/Memory_Map.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/MemoryMapPlugin/Memory_Map.htm @@ -597,6 +597,7 @@

+
@@ -608,7 +609,15 @@
+

 Auto Updating Selection + by Location

+
+

The   button controls whether a memory + block is selected in the Memory Map table when the global program location changes such + as when you click in the CodeBrowser, Byte Viewer, or Decompiler. 

+
+

Provided by: Memory Map Plugin
 

diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/SearchAndReplace.htm b/Ghidra/Features/Base/src/main/help/help/topics/Search/SearchAndReplace.htm new file mode 100644 index 0000000000..3b261ce011 --- /dev/null +++ b/Ghidra/Features/Base/src/main/help/help/topics/Search/SearchAndReplace.htm @@ -0,0 +1,478 @@ + + + + + + + Search And Replace + + + + + + +
+ + +

Search And Replace

+ +

The search and replace feature allows to users to search for specific text sequences in + various Ghidra elements and replace that text sequence with a different text sequence. Using + this feature, many different elements in Ghidra can be renamed including labels, functions, + namespaces, parameters, datatypes, field elements, enum values, and others. This feature can + also be used to change comments placed on items such as instructions or data, structure field + element, or enum values.

+ +

By default, the search matches the text anywhere in an element ("contains"), but it has + full regular expression support where + users can easily perform a "starts with" or "ends with" search. Regular expression capture + groups are also supported which allows for complex replacing of disjoint strings. See the examples section below for details.

+ +

To initiate a search and replace operation, select Search Search and Replace from the main tool menu.

+ +

Search and Replace Dialog

+ +
+

The Search and Replace Dialog provides controls and options for performing and search + and replace operation.

+
+ +

 

+ +

Search and Replace Dialog

+ +
+

Find

+ +
+

This is a text field for entering the text to be searched for in the selected Ghidra + elements. Elements are either the names of things such as labels, functions, datatypes, + or a comment associated with some item such as an instruction comment or structure field + comment.

+ +

Ghidra will find all the elements that contain the given text. To perform a "starts + with" or "ends with" search, you must enter a regular expression here and select the + regular expression option below.

+ +

There is also a drop down list of recent searches for this Ghidra session.

+
+ +

Replace

+ +
+

This is a text field for entering the replacement text. This text will be used to + replace the text that matched the search text. For example, if a function was named + "startCat" and the goal was to change it to "startDog", you would enter "Cat" in the + Find field and "Dog" in this field.

+ +

This field also include a drop drow of the most recently enter replacement + strings.

+
+ +

Options

+ +
+

Regular Expression

+ +
+

If selected, the search text will be interpreted as a regular expression. This + allows for complex search and replace operations. See the general section on regular expressions for more information. If + you want to anything other than a "contains" search, you must use a regular expression. + See below for examples on how to do this.

+
+ +

Case Sensitive

+ +
+

If selected, the entered search text must match exactly. Otherwise, the entered + search text can match regardless of the case of the search text or the target text.

+
+ +

Whole Word

+ +
+

If selected, the target text must be an entire word. In other words it must be + surrounded by white space or must be the first or last word in a word sequence. So, + when applied to renaming elements, the search text must match the entire name of the + element since element names cannot contain whitespace. But for comments, the search + text must entirely match one word withing the comment.

+
+
+ +

Search For

+ +
+

This section contains a group of checkboxes used to turn on or off the type of Ghidra + elements to search. There are also buttons for selecting and deselecting all the element + checkboxes. At least one checkbox must be selected to perform a search.

+ +
    +
  • Classes - Search the names of classes.
  • + +
  • Comments - Search instruction or data comments. This includes pre-comments, + plate comments, end of line comments, post-comments, and repeatable comments.
  • + +
  • Datatype Categories - Search for the names of categories in the data types + tree.
  • + +
  • Datatype Comments - Search comments associated with datatypes. This includes + descriptions on enums and structures, datatype field comments, and enum value + comments.
  • + +
  • Datatype Fields - Search the names of structure or union field elements.
  • + +
  • Datatypes - Search the names of any nameable datatype (i.e., does + not search built-in datatypes such as byte, word, string, etc.)
  • + +
  • Enum Values - Search the names of enum values.
  • + +
  • Functions - Search the names of functions. (Note: This search does not + include external function names.)
  • + +
  • Labels - Search the names of labels. (Note: This search does not include + external labels.)
  • + +
  • Local Variables - Search the names of function local variables. (Note: This + does not include local variables derived by the decompiler that haven't been committed + to the database.)
  • + +
  • Memory Blocks - Search the names of Memory Blocks.
  • + +
  • Namespaces - Search the names of namespaces.
  • + +
  • Parameters - Search the names of function parameters.
  • + +
  • Program Trees - Search the names of modules and fragments defined in program + trees.
  • +
+
+
+ + +

Results Window

+ +
+

After initiating a search and replace action, a results window will appear containing a + table showing each search match as an entry in the table. At this point, no changes have + been made to the program. This provides an opportunity to review the pending changes before + they are applied. The changes can now be applied all at once or individually.

+
+
+ +

 

+ +

Search and Replace Results Window

+ +
+
+

Table information

+ +
+

Each entry in the table represents one changes that can be applied.

+ +

Standard Columns

+ +
    +
  • Original - This column displays the original value of the matched + element.
  • + +
  • Preview - This column displays a preview of the value if this change is + applied.
  • + +
  • Action - The change to be applied. (either Rename for names or Update for + comments changes.)
  • + +
  • Type - This column displays the type of element being changed (label, + function, comment, etc.)
  • + +
  • Status - The icon displayed in this column indicates the status of this + change.
  • +
+ +

Status Icons

+ +
+

The status column will have one of the following icons to indicate item's + status:

+
+ +
    +
  • Blank - The change has not been applied.
  • + +
  • - The change has some associated warning. + Hover on the status to get a detailed message of the issue.
  • + +
  • - The change can't be applied. This status can + appear either before or after an attempt to apply has been made. Hover on the status + for more information.
  • + +
  • - The changes has been applied.
  • +
+ +

Optional Columns

+ +
    +
  • Current - Displays the current value of the element.
  • + +
  • Address - Displays the elements address if applicable, blank otherwise
  • + +
  • Path - Displays any path associated with the element, if applicable. The type of + path varies greatly with the element type.
  • +
+ +

Path Column Information

+ +
+

The Path column shows different path information depending on the element type:

+
+ +
    +
  • Classes - the namespace path.
  • + +
  • Datatype Categories - the parent category path.
  • + +
  • Datatype Comments - the parent category path.
  • + +
  • Datatype Names - the parent category path.
  • + +
  • Enum Values - the category path of the enum.
  • + +
  • Field Names - the category path of the structure or union.
  • + +
  • Functions - the namespace path.
  • + +
  • Labels - the namespace path.
  • + +
  • Local Variables - the namespace path.
  • + +
  • Namespaces - the parent namespace path.
  • + +
  • Parameters - the namespace path.
  • + +
  • Program Trees - the program tree module path
  • +
+
+ +

Applying Changes

+ +
+

Changes can be applied in bulk or individually.

+ +

Apply All Button

+ +
+

Press this button to apply all items in the table, regardless of what is + selected.

+
+ + +

Apply Selected Action

+ +
+

Press the toolbar button or use the + popup action Execute Selected Action(s) to apply just the selected entries in + the table. If only one item is selected when this this is done, the selected item will + move to the next item in the table to facilitate a one at a time workflow.

+
+ + +

There is also a popup toggle action + to turn on an option to auto delete applied entries from the table.

+
+ +

Navigation

+ +
+

If the toolbar button is + selected, selecting items in the table will attempt to navigate to that item in the tool + if possible.

+ +

Double clicking (or pressing return key) will also attempt to navigate the tool to the + selected item. In addition, if the item is related to a datatype, an editor for that + datatype will be shown.

+
+ +

Search Examples

+ +
+

Basic Searches

+ +
+

Without using regular expressions, you can find matches that contain the search text + or fully match the search text by turning on the "whole word" option. However, to + perform a "starts with" or "ends with" search, you must use a regular expression. Also, + you can do advanced match and replace using regular expressions capture groups.

+ +

The following examples assume we are trying to replace label names and we have the + following labels in our program:

+ +
    +
  • Apple
  • + +
  • myApple
  • + +
  • AppleJuice
  • +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Search Type
+
RegEx
+
Whole Word
+
Search For
+
Replace With
+
Matches
+
Results
+
ContainsOffOffApplePearApple, myApple, AppleJuicePear, myPear, PearJuice
Matches FullyOffOnApplePearApplePear
Starts WithOnN/A^ApplePearApple, AppleJuicePear, PearJuice
Ends WithOnN/AApple$PearApple, MyApplePear, MyPear
+
+
+ +

Advanced RegEx Searches

+ +
+

Regular Expression can do many advanced types of matching and replacing which is + beyond the scope of this document. However, a simple example using capture groups will + be given as follows:

+ +
+ + + + + + + + + + + + + + + + + + + + + + +
Search For
+
Replace With
+
Matches
+
Results
+
Red(.*)Blue(.*)Green$1Purple$2RedApplesBlueBerriesGreenApplesPurpleBerries
+
+
+
+
+
+

Provided by: SearchAndReplacePlugin
+

+ +

Related Topics:

+ +
+
+
+
+
+
+ + + diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/Searching.htm b/Ghidra/Features/Base/src/main/help/help/topics/Search/Searching.htm index 09bc57e039..7fab4820ca 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/Search/Searching.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/Search/Searching.htm @@ -17,9 +17,12 @@

Ghidra offers a variety of searching capabilities.  The Search Program Memory feature performs fast searching for byte patterns in program memory.  The Search Program Text feature searches for text strings in - various parts of the listing such as comments, labels, mnemonics, and operands.  The Search For Strings feature automatically finds potential - ascii strings within the program memory.

+ various parts of the listing such as comments, labels, mnemonics, and operands.  The + Search and Replace features allows for globally + searching and replacing names or comments on many different types of program elements such as + Labels, Functions, Datatypes, Enum Values, and and many others. The + Search For Strings feature + automatically finds potential ascii strings within the program memory.

Related Topics:

@@ -28,6 +31,8 @@
  • Search Program Text
  • +
  • Search And Replace
  • +
  • Search For Strings
  • Search For Address Tables
  • diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchAndReplaceDialog.png b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/SearchAndReplaceDialog.png new file mode 100644 index 0000000000000000000000000000000000000000..d3e43de1d0ded710dfeca90b6f2d2b7453e4f9d6 GIT binary patch literal 18914 zcmchN#@ zJEK@nh?(UWfBn`pY}Dwi#7w7*p;AyYy{o%sZ@_s;t0l+i=&maH zv08l9s;%FT*?<4u@i&5rj_!qGaE6cXy0S+KD8}-uBIacdd=GZ-l@8B!l;%caHB(cq6Y1?JR0~^#Z+GJip zg%t1GxbOFS?dOSD=7wZXHd!x&7FY4(+sQz$FC;gH^#_KLLujgX@t#S+jARhR(gqg0 zLQEvYdaT?mC*!Yg2WPE&W`$K(`S~6u(bGlnSn1;69Pqi!*y@Sg`G@{wL}4htKcwFc zkFK5}q{8%i8gjQliM_M_C8i*>FEtz9MxL^;r{s3cH*x&leu{RCBq)9`F&$gO^niM^Rzw> zhhcq#MIlTVN+&liw7Mil0_6Uc{;g_lK&35y{4;OfIBtI(uAIj7TvZPRbR}L~;Rxo=f%F9{oz};BOi=|}IHgLeuKcK+hMSrf0h4H`>rT&rWP|ph6u^GomjES8)xYoq#I1lRPb*qF*bOnP`}uQ?{6I{Dbl>*K*g9f{My;=}m~ zbIz*#bz5q~Dv^eq5WZ%v53ka%PKroEz!7lwlfKgvZWIh5+|;x0M`MBF>X4F8vhr$T zT6_@+4-YXiyDZe7tM4e z!QS@mdFnVWZJ4s}LG`)2QERfVR~U|NT(7IyF>0W~jI3>tifWl2z5A_w)r2K~a!~FD ztT5E_qW6dcdxY@Eh$PPWBlbZxjW0~m4BLA?vS{{jrBcP7A+OQQDd7!+Z+VZlggYuL z+fET0h;FXRy11(7N{5suO%(m9@U?!2>(u^2!Rqv)n?=b4$ap}461w(r_Lb7W;GA6r zhm?UyGWRHLv2XKTl}u_{;Je-XO@slR+#9ox4-Lf=wtX#XkW$`zc3yin9-0MP8MWIG zZ;t8ZtxTWb45eJ#6{KegN8)^!KRF~f96z+Uc}UAy!X}|1sIClHIxiUC$DQk@xsTr;{q{7>*Q-e76qWt-Bm< zrU|#Y683p{22(3+u`8qAfT<0pjsa^`?)P~ytXL6badH6;@o2iF=#VUgv>b;C^(QW`&rU(feb41%dMTDMTsX2Oxg_K~m^*wWx!&dbtd87Tx?-G0u(PnyX9USV@e7hD zdo|Rq%4O}&=IO900rT6a{cTC+8(PdZC)&b^#(}Of%8UFlP@V9Ki6h~2LqCA;8)=4H zEFH0VxiyHHnau=46nYNVyksP75kvJ_y|U)5xweX%o<`yt_KRl_LvHwzYLKPvH`Cnx z_AF9`9XY!y1T%=A*#P;{MdYq~%G0qO9`@YykwuhW4jDMPUwIi$CV?I-Qz|QWiYMTv zo``#thsgoNp8+N6h3d8J4wh-kEVl7V-QF-dU6vu%GcO!Tf~S_5jDE~NjGI4C#D`RO zR1?tt8-BN{KxPe@oNASpYK7b@CJdzx>WI#7al^D<&HvM4foEr(Odr1@Pd7 z*z@PlCrw1=Sd1~}*;-}Ux}LQeV-}F6baWAt5%5EFS1DI<0E$4qg#aIejAelKg#Sx> zM;8G=wsQbY_9Fj}KgVo1D`a@#1W68Thv*0_hT*An`<36my@e2;?D{T|?*DX${}!M- z4qvuA9F$Tse-joRK$7!6T7FkJrH2m&^x)yHT$?L{4Y^}dM5u{XbJ|c@{ zGaK@R^O6Rk@Wa;Z%Ho_yxmB@!;i)D~1}(_>oXHF`024Xk zshp^k(cf`e^L4ay(>e~8bXJnCy<|O z(*V~Eis2d-Mw2nyiuA9`LmMQ96%GXq>sKr;f6sq;p=@7g;&n6}${&1l1A|lG_K5al zeKYeAE@JSt$~xar_nm_%{R zH_OJ5zW&XX##^35!<$C3(NHV94Ro?jX-f_(N$=Iwuj^fJTutRfHD%pJ=vkkfF~&Lf zEEx{YtM>(u<7?wg7Yjtp3eDtp7x_wLNdw?9Zq^*HuC@LYYM-p|q_r#bl4IGy#Ao&XavfEV-+p z4>r8?>Ky?ig@@l-gm1&SWEmBF6-s7aXG3!^7s_`(p;%jGy;Nh3G1+go*{0so>_91C z*z?y~&W3Bt+DX9)L}Lk86;tJDsn+VJ;$fzj9K`is>@6yWfnGe#oqf;=0E=Z z@k>v$`bk-~R{uM$w`H=JKZ7Ksk1YmesD#Hu`IC&)(eP7mAy~%09Iq|g_|k6_u}uJW zIx99L0n=yu=4oI#C_cq;$}=^Kp4iQZyIdba&o+`dUqm5n*Y<++S^F z*8g>4cJ2!h6lTSzPk#bw76#S?YAA8Pee?jGj8;3jP%ib}<%y^P?xwI#iG%I@=hvU7 z1@WIIcSF&;*`_FQ^R1PO6M+4W7gOzo5&Wl#RR1B|lvFZ`*SJOC(#5fO(|qN`(U5wl z=PhDHoauA2;}jNH&}Trn;Husk$&=?qS%`&v-)6O<3@zPXAq}pNqk)`LH!e%{ttHv& zSC+&jJ8aY$x2XOeoO9!v3B~58*xOtRNe~sln-x)Fmq*EBI+kSBaYqMd7lD{Q4z%REC<)rg@EPnwkf9 z9KC#Km4n+eIoan{b2z-{@#u=y#L+H3|f<=(pfD?77a0*vAZfGOrI0EJJ@=gsrYloxnTL0k0 zAC2w7Ku%+yz9>yEOuprIWq5eMxAIhFA$CAFRZ+-yZ4Dds{>YEc>hVj7dKu52ZqD$t zq1(Rwm@ybA0=5vcVFf;2$yB+X@vJ{BfZ0^05JNCny&;cCdpsRDt{1zRqFvBGc}Eig zB-+})0~zN)qCP3LceU3&-09IZ^tpy(8y|EqxbI_#Moo%FhJomAAo=hJB(l2hCGV=n z)a6tQLO^<)1HSBB`;2^nEsj412D%K}a1%xAeukV*_l~-w+K%o2X^J(_Dems)4vz zH?Gu~MD$O?YnR2}@5axlrl*imt<5%*Y}0IJsUfi2nR*kTbMCClYkH$SCkm4-CbvH4 ztOwj}uPnSah?`2>dILMQ0`HovR9utr@K&z1s?NZME>L(|WxK7a;5{PBizkw`U8s%R zc|aP6jwugG@DBlDTCyt|&iD{WbkTRmj4`CYBTI?QLHOLMt}LR3Pls-Y(qGS+0KA|v zYCYt+>7MgXWHJ!#MOq93G!-puj^S}lC;!`E(f=Okl70+7!NQ2=rlXSfuEux)*nQ9dLURQk8=8?uELu9<7pHuglbkxA_{dey%s50FJp$ z&0T%&JMH#KOf#X95-|0+q6S^ejj&pO_%M>-oNIDRt(F`%a_TT09r}zh>oh8;GOUFj- z4mS~CF)ei7@_v}x8`cL)g@bVcR^nrw*E<7b#Ny1wxLvq1h>-D3hWkosBWX4W_ryFW z$>c5y0Pb~@u1Y=b+Ce7+s~@&cg;*NxBQY(@sMcS(zt+`bF2Wc^{M&qEEu6frA7YVT zJ6S*5I^;5beeI))aKVK9-wETR2W=fl_j;+$%e#cIwtY`ZrbIUH+QQy_7ru03sg zH#OPVVBKU!Fb^Zg*Wjwm-fg(*^a9)_rM0PNRbUl?4>f%INE3IWjO2#4lRlauO9?k# zu|@ih(dDWutLL3uiQm$D|I0wMLw#9h#vyMswA^yPyr>nkXWajc`)k$QCEsleOOLa! z%e2KD$?L(SiiF#ihhuz+o2bm(&`KbO>kwt`2~JZYJ5<=+MU>0sqN{H?p3RG5m99j% zKhz`Sm-McO!Iqhe;eNoxP_F7+WwX~QW5bBWL9fNdYct?h+cS4~#(rhT>pM*+<%t_y zKfROP14oLs+aHqMmz>2}#vc@O^i zML(mR$h$14c5%ao{JbPki+g;hJIe_Y^wJ`J$Y}U}wphO0r$XQm)n;9P->3itL*~7(4 zZWAZXGz2murH)^IYH(-OuTVKxS+9V>KD7XunABl6to?fJip+QA{CMV#Dh=lttW6mc zpAN2gfo*715n<0dp%-McIIuQK-M7S-t}S2MX4{s%wPXLg772h5cZb)S}6*F-GeqWxUvLOwWGZzfVJV^vmTm zaR==qo8wgUl^b3>SiJiBg(Lv1l)vRDCfp{t-)Gf&_HH&Jq$BrM9tnMZ zkIVl_u7w$Z$_Pl#xlA<~a13Kc%?ri@3q47aW451E^-LYJKbBRAzBgVx(;Kg6g?FtZ z?*nnLbU`ooRsL0@o61nKEXrQo!!$XUkZrjAd~EE!DZ(mcTr+|yii!+lhL;3dQQGbRG#6GtmAUQxv!LF~e z&t@6z7+xJM?>?m+5Z{I6O}Uj?M0n(M+P=$)7aGu2Tg{L$@|&$cXFd8h61Or=>iX|?Au5dvND^%I_*w0N54=aa@zbI+<2#~14IoRGiSr_x#k zNzYB2Izt+$@0fh$O`Rs>CTeU=lsToSKjl@GwohKBA+UW?q3Ml`+2xH-9Bdn~EeUa=B$C^^qNLIRRUH{9}Y#A2l}4Q zKoIXyhnv*8!cGtXH|Hnl`P{k7=9$oZebT8NT$piP;fR4ff!l)gO>%1ah++fp%Z}KV zj*0pV`E4$CsFW;>N6Oz07phsEQ3wWrmCnO}E?CUYKc1ZjPk}WX*yKl+k^{LGJ$$ym zpDOzJQ9Wi(|1PRD%J+HY((*vmz5Ygxt@{(ap4>$?4r0?9i$9Mte49CHKP(uo=qB7f z>;v8Eg@$Z-ByIFUKR4Jy?}A-1aVg0&-4pDbVB5EYJj&|x-ldNP_tj;Sb3+5vmYANH zu1~xpQbVJvkzX!@Z~V)n_bpK9Y^Iaqf``Y4?gjA)S&_b_=Z@u9#+Z0OHty-0e9f^( znu-@Q@(KX9UU9*xlq4-&SZus!WjGXmx7P~q=E%#Qx>|~tHvYyh4k46}b@iJ3htg z%I7G1@blj$vARH(J+6DsrmTsUZi&~=*&MKCMHk*N(r{po-v8MHcQnvQgu|I{uZnJ#}sW<@WPtZ~b1yTpeXNVb?OU?5!CM(1af7rivc2k3h{`<&&*>MFlFQ%i@C zqqUbO;%k9ZbdjZ8$nUMs4(@N^g z?kaz*+j~1;qX1a+>fKjzR*r5auwlU~!^qFRcHX+;RfK*xIt1swm%FA1kaX!Vo*9lb zI`n8SiLDvL;YCW*&M_2UrGR3cmZ};>lgT)a`n+q;5CWyZA(a z>6&j8{%va508IxdwK>T!Q{zVZmLbnO(+6k28!&^n0}N^+N#n{Jy;)qVuw#55fn|;U z1Ec}BC_eF@!L2%v=Zb4}JK?f{f< z+^sC9FOpgXmelG1oqi^(xA3SSxDxb|P12P?){A+kF%+xe57nJhZH%^v_hpsGVIT6! z^cRi;J!9W2=04S%Nre9!Mus-k%T6J0Gn-=e<9jVpNYO!=-aH4~IW^5RSsp3<%!L|u zz}=4=eYjfUgS8{j{j}tKBazD3n&k3?vtiL*FRsq!cxRg#hTvP$(~w4?&o5ntIglMdoHaCd1~3p# zRB35{V_wSjOVCighgsAz2i$UC(v@w872kcF`A*Zj9)RCFRmvJ|c%%Jcg)eAYX$P_z_lK!G*efZ_0n-5l30hI<6|Cd>6{W95c-&NAzxNVNRaK*GN)_&5Pc}lg~XItoZZ<=K?a$A`*HXZZ2;_lOJ6_r`5Q1sT+W`g1%*%oQjaNtK}uLY!=@cZR#J ze0$~Hcn>%<~($U&0%Kk+fjbm*p)j zz|=hHO@_gH4r!@JOJ;BhOB!Cl-fDWQpcbCqd~hoUHJf1<`>4>PXDHb8G5`a(v%$jQ zCz(Z-KeGt^H75mjOq;;Zk*3weo)doO+;k;IP4%sIT!qno)n{JB7DA$YS+?ZflM{XG zSlKyAF}IekXH(yx8$%#GRrp3_ufS>d#P^=$x@4Mc++$x6dsRab->5oAB z$kY?@#@Ew2tXUXC+r=@irpZaPVc)Q7w8Rnak4hr*$Y9qJgaX%|d0n3$vQ)dp12o*N zb^}+;FF(q|ty0x;h^-U}UrFcIXzqDR>%eYt&wnpRkV1SDQMp zq>PF|tw)GQ-qAanY0%lTlGC#eL}=w^%Bp+o!beTbgk$O*`%-w_QGgoknK#iXQBr_S zylKulp~OF_^=y<8CRWVb7s0X7r^(s!2}HJv13cZ%fb_I3%S-+0O4I&*TZudDI^pnL zUo}s`P}Xyw3$sp!lQ^yDwVc*Yo<;;;_*6#iCxe>z7Ekmqa3-g(mX-C$;$7vvG#<5) zW747Q2iA&bS6=n1LV}w5c!QsZ`b+UEp{71BO|N>)K^5N~Z`xu3jP#ujV5B@2U0j|r zur2J3@rupA2cxF3FV{yw@3$+ismHKX{P|y5V3viCK+`hnx zH+empYbT=tyGwGAH2{2>Pz#)Qt&Prp2PhWDd)$}r@J;5H+3<#~DsF2rNl=%a6lsy` z^022`LxnMh4=)uB5GGS&PNOZ#i~$@YyNYhzOA7Pv-5&wC(f-6aB4oL*4tQBzjW9Yb66#zIS3U13|aRYc0)16I*Kr5`yQftZeCMz+1O;c=`%FURv1=xdQoRwGxPXG? zPj`eFr2Wn@Bq6uT1^1;HzYLd(UHitlu(1Ko0>HlYT}()#01RwKFLe z7*1qE!2TD^X6ae1%3$qjZ*v(-&ED=JOW<4&2kpsv*?C`D;ZXVOYZ;&hdnR#!#pRyt zPxAhI>$lXm4${de^+(8z`pgtgu6}x2P1i((Q|bVVng53$bXLLFkJ^qLR|{v)8XTbo zYu&uzSm7?K8$UMNn91DbP)hgpe2fq+Fc_dM2T3=gAP2cfV>0a;$c#xf3S8TLBC9My z7U}YL1CU`fbsXF07+sAt-~$)`_HLFbjV}M9igz5fHokF?tX>iMEGutSuvcWDd|=aW zKxbZ*bpkC|_kkT;X{(4e`#astE6*P8sJdl9L9rb>GG#mEkJZ?d-TXVr+$=HMEgfo@ z89Wx>F$+`-j?o;Loy!ja$jY{KBJrH}XT|GN9( zO^w&&nA<)<&VBpSP3C}di+f|=pf>ziq?lcP9QgFD=S?dIwCJT!qKm8C4jhlzSasPk zv8HSZuQWTv4I;$b*D`y-w^5k`N?DJ3e2UPJQr=kYa6tPvvm2^k?O7R|>yj4!zRbN7 zEKUYcS;Ev4B7`f3cu7FpwM*v3BbJ!>q>}wADiLm$m~$-sINVYD7PgS(&3S6>hh>% z;plgOkcW544AvuN5sD6IS7pX1ujHL`ii%pvsaTq{I7l~YnY47HhK6OVUb;EuO?=q~ z?AP{MlQ>dY$cnRqLe&L$*W8b{Z3}M_r*X_qhU)uO)~~ zhn)7^KsW^MJp2Jj)c$XQ@S(FZMr9leQTNsqsLcSkQ?W6d*PX_THx1$UWIvM%EjFPE z+Qnr6Z6;W#<{BuH>*wxNOpmScvvtQ2+Dh9M65p_vAI7^*@zw~; zZ+k_rI9oO1udxX#ZJqA@26Rg9f&tzB7Cc_DRUtXTS6Cj6%2+>btw^)_u!7`bq-+EF z=k5@S!HLre&b2A}>PVb)_GV&|O8v2l_rE40>+Zi9MO2jd)>5V`GOl+{-LtQ7Hy!JF zu82{dn&Uragb%0#3h#a^zfm`62Y~U#Ns+%Y=U*Ejk^IYJbQJyNz#^(jI%pOld$`rY zd*5|AcE~Cd+@~L3oJxx>K0}_nZ6V==MwO-V=F8p&OE4;x{xXLn>kLn~rklEz2!ER| zii9YZE#C;#8L?X(iDRi$e%URg)0?JSO0*K!YxC#;@WN0TVeRHai0ey_^*y9a;%uXh zQYi7WeQ~JkIi=P;N>4M2Ep}Vsu_*n9MPdm5YHtP2$ueq4>WB5q4)r$wmj8lk{U0}I za4|6%FPyz3*4KrJOyHEHM>R!%URb(G+dSZ1EPe_LaJq2pqMuw|ax~Zh2$4bOX|Cx` z`gBFGVNQSU{rl#%?W3)25eF}wZ)vgq;qJAn=xD-Tv;XjeGlPqYy9(o>SfIF}OwU+{ zaT+Nt!vDDtX%Sw_ksJLPP;F9OvxSD)}IpXR;z zX95EQpMWz*H|4$)`0E9~!N7gvtdXXZ>{m|p`5De|oX9(}pQy4B2%mf&E5s2?b8O(W zVE!`&|MNqCe`43&?Rc~xK)DZhnTPLI@(+yR8joLoNpetEPVTmw&+N1qnf;h_1kpRN zKpALxe4i3qSWPBBm+y3}CylT#GYn3#PtlEE{v@t&6#ijIe1sT}1xF>%Z zFMw=+BFT%E5MTp_Q2a&Y8`O`MB$jd^zW|g^&?~T#B$N&vn>oD}e{tGJydOELyuLe$ ztj3eJE#}Cd{DNn`0?7j?tNQ?iLjQwqt;NcaY-{nvr!%~XWin}X=m27nug~<9q=UEw z=Fw;AHQ;Dsjttx~OoSuKH1G^y=UM7=8>5RQ7iYhIOlAc;Lk$I$xP}~n3PXAxr8K8h z`6WRks^C0OE_=$OH;Q1E*t@=EdG5l*0_VllT{9x4X>oG70OOt1ywSxA?htv7{AKr` z+EgZP{%cIukspA}hm^S#xxN}w1rRSV(6B@O~ww%P&|t4jl);m0(kkd_hB- zi_OzrnscY(JJdp!A9RDH;7UMAL)?P&FOEaT-ppRym|X&|?u#hrP6;1BJ8}Q(~_KoL6}#6t~7P7?Nw+k~1}o zbz06V{p~flaNUIOiD-!y^_oLvj=JUaW$qN0%Obl+d%JUY^xb>zu7WwzJ11RG8T@HX z%X;>2CUReal3fQ+6$2&5tBzm!D)3xiB~hy~J#j=W)@4lDpXK|B;E15!4<7%uv}g)YBn5OPfQjYflgg$(S3@_0GCd^2Eq{?W9;b9V$7G_$4q$&~ zByW0WOR7S#M1oByW4o=i2~y;G1?sflJH{=`(VaP&^ym6dfndqBICo(Yq|PCw(8CTe zF(YXK#JXlfWx&gb*!_6(TGLqsF(qJovT*L^RVipg{{fBjBW%#J_wE``}G$v5Z) zPV*IPSxa1p4-IF8A{S)N~p?>FGDwoK=`e0{RXp3D<* z3(E`%rVhi1qC)j&ruhXSCq8N91&<^;OmdHH#%}ahh1iZe{c#{dV#nIa-tNW#6_n<% z3}R{0?$;5j`pcFS*fA7rG;FOXL`lXODbixCHsv(f$!!?!F)bCiJ)-b^uR>tq&sG4` z3gNqqc4S&0#qGE=i`qYg%WnysClj(P=)YiETH*Bce>bND4CtWie@`6m9dyeE#-(TtD&=ikweOhLrTeCJcQ zv39{!T{Ltr#13aSoR5Dr$E5yF7Ku>R&qw!Yf{Dt(@&ql$Ovwu%4AzgI&>LkO zJfLrHH<=~PWE>eEo{w`WG@~gWTdqiNC2Oeg|DdOuX(v44b)I)T|LNttST~PbY~16K zCi5oD4>Z8g7L_4v*XE}TiI?<5nDaI3ff^2=YHYi6TOA9?4vx4cRR-5UB2no^UqMF z3pWz_1Gmma3=lxqp-~^ZT6!B1O^?I(*bD128D&p@;b=$yKdhy@30Nr1fL> zG~6NfGYh|ZauPqc)isnp3up(A$mz=W2L|7QuKwutKF2uP#bkIq!>~b?MF^BpI;1OW z*%5QLPm49*&~mJ1^ug5di+cJF4`}c(roL*cqBo307Erf$dpA-%=U2a;Zpccy-x6uHt- zHCX=Km3u_I%U%{&xVy`$@sS)VMHR#w5)b{Ug@@#!~M#bXTsDMpB0L7BTAF^ z2NOV7X!0#!NY%2odkYd&`%muR-Th_M6-9Q^4eb?d=NOO0<4}38W~)hWTPD>Tty^cy zB%Frdc+wz~XAUI8m3dr&(k%3Op7Vi=@J`S$x61lx2w(IH~EgNg3jkIh~k!v;I74d+5x;X)JG8|5Fti0sm8+kfFVE5}-SNa9$l;%*{=N zO;>Eqx&2(Q8a*g#+=Yz^i!($t(&_~t(Qp9a!+7E%c80rl#>P`<*#_ww4+=fp%ZRuE z1xgMFvwIS$voI5>hALde1>j2hJ-&mq0%Dj>`~5X4(7)h6M8ZbIQUk17k2 zZ#fqY5?cx)6c2JEh$Kjb4Z+piH3c|~nw|j{7nY?WObVR`#v=h4u2L)w!y8df;1A?B zY0o&J!0pllMg9;G;66+6wc<0DRn`qA0m4%UMaK#~H=43{Nh662k6am7M!rG1{=xem z#e2rqF*9kP2DqUStUh0JxOd9dzNGIFSIt=BkG-+m(m@6Av84Hrh^_Lk?{O29#k{i* zFLdL5vKjsLJ_wYn>=?_CAM4kww_siCHdfKDex!jXIH2B`zk6R}b#nPfRqZ;BdadkZ z_q6k5o8cfO&L4B7)K6EXksKC9i)ljL#Yb_V_-gte)2qAN;%ePE`P9zfjMV~`Y6yhD z?Fv1yIOo$Br!EApMDnu)jR7YIhy4*j7H*b%Xv+sqt?>@H$>AvWSF*ruKi*C;+VTHn zL2{1%6H4THOD?Hg5y#ahlGkzRX}WmE8LffkY}2WBPw9=0->u74@0eQvQU#JgxQo#= zG0d*NS~E5!qVos~yGfxuM2xQ5YxI^vGQita`GgxWzVT`H%-#r6W?;;o4~fNdPozCs z7!OB{PrSIsHSbW{Pp>LM12%_AsR&h%KFY{Q?}Yo($2wA=9A#}ti?;yM4pf>HV#g-b zX$1a*AakF?UXAMH2T*8QYLQp_<7L)pch~L`aYGD6jQq`ymn1j85NH%yG`141R9kv~ zk8{c|sr>6jH94;rk9J19tFD4+nfd!w9DuSWwz;ZOGUf-Z5Ppx0k!KuV0MQ80(W$+SAgB2G$XO*YGCm z_t!My;>yr};l<0_hV_XXz4FAuWL!Q=m`I4qmlpAU%nTY-z}n*HTy#=V|blAhC@ez)>X9kmYxAl=YSKZP)m#;$)= z)7G4>)Z5^n%3zbvo#oyn6g~Ppjj(q-R^NHo!ko3>)LvWykHRmxns4C9;g>YorfdvA z7O8s{=NA6;&c9~hpLa(^Eu17!m)yV4dFB4AEir>)pm@AwlQLHS&7ywbiComp=bnBO zM9HL{e|{OmBTA?XAXrPLao=T$J2~>LrVsf4zuNG-XmNLJ&$IH^ja(~9;GP2@j5y*u z|N1zrS58UU5;&({-VIc*RK4Bq{k`SM2|#y=DDs}{XI5W7g6jEJ6JL(S`VWbfsU1`y z=8wbfc-wgf`UC?cvyQ{_o0&HO)c_zan0`CR`=>#GeIaB^h1a9T(l5;_RZQ(H45^j? z)FvQ2F9}y$G~lfbf7a8JnEkKu*Kwnh>^{wrGOX4hM)H=0W{Z1c{oS7FtEv*RSFhKF zz)U4n#|SgFqOpo;Bi?{~w(8RD>*Ix0B-em<&n7x4C`)n`)or4u*<})m!zkT83RGW$ z%sEdPy1(i#2D;k!SN2t3-F#^JoXGo3H zFyA0eEWKevIdBn~Ib7U(|IZ|lM1Vs#;Y>K@d6RMZ%8WUl4a7Eq7uL78lKuB}?iUFG zB=M!|LGPRVT%F(*JcAgcU~cz3%Ku)6W}sv^*?k`t2*)%utx^&7J&{1wzbws1~90+O+uh`AuDU1 zsbkO|k2t&f!KT4vBBXw{+q4pP3Ra9S2LyaOF<_WWH`f zaN35!+=Xn#m4{o4m(a9X*Ql>4yk0}KmbsX z{$D>-m6lMfNI*#tFqH5n&kP;F(gEFN{IP$V4g9uuF1JWrV|q!|Ka-4nGCZnxQBN>L z)z8r%R`~V*?8bY`7$*sefP-L&fUVy@4YdB4?cDLzT+*mTsG9s8F#Nr2 z!~glB@5i7nANJwH_waX4maM9IWoC|e(M_J3x8OwAspvHYV0P+_aUMaY1$ct2o zXIKEe!rdBAIDmGq|ko~*Y6N|$X94-U~TvBlXwDz~s z3PS){tcCqNRL_8FAJF3dH>Q=RU**Fv-8#Dv;pez=v{3zj?`F z;94@RtTnf}N&-#-W?*7pZ=hOr)M$h@+g~#+1`u%~A%8Y8>9V&6LCM{)2av2O#tS#8 z2XFeS4j^0`MxssJ+sK%o6%++P@^w;vVSC{d}Ke<=x@W_oA%jj|svC6#{ zACmZu9a+oI2wefn&_fs_n3YM=Yq++?c2StaSV+Bmib{OLrhuqMu>8Fh5!bhB@BX!s z+B2#e{JPCm$H=MjXMrxO`sSk$0^gfA$!- z=oLC!n|RiqwiOe;r%3hpe*nIuzN_LtjcC-R-)al-HSL%KSw)Tso1kt?JX!uY_+Mq zw;1*4DW-)l0o~Zohjy zQkQ(}k8*N1S98@T9a3{YV~I@f-@Hy3*kp2l?mDF5OyFEbSm5u4n+k z0M3G{zZx zd1^v_E)i_k5mY)EDkHTw3kleeCM~^o>=@mLDxUAKAxLFpgmEF~hX?k!&l+~t@FlXg zgGs)e^|i9qC>f|;rrcOS(s2=;sF509zyfD@aa~iWyR@?iPsRr$0w$%%OC`kk(Jb8S zCu5vVM%?R?k{^AfrlCc?pKp)4DMX+1^2#|&TM-SZpp z`))NeH@7?3q?KkB_X5t`rzo0A_KM#A3SjHVdehvT5F9f@cH9jP}C>Htnc7))>jC83$`tcsWJ+YGvDZTx5r~dXp8GBME zN5ALq0}PUyQ$<)wXG<5@;)$wEpGWgnGEq3$MCYj;?^UpAXT9k;3!8sZ^?@jgj^UOTS#%Ou9i2cP}*L6;h z=c5RuPnS88S>eav2nIog?LbN?s;)Ofm87@Ys7IE~FViNSckeU?zVS);`Hl-Omw)(b1Z>UeOC?cu>3i`-12cZx-FLw? zN5=$7_1?xw@~{q~pxriJ?Ugz|v41+#_Ka8;G1{M@NOsS#T8Ph(w$vIroPNh#IYyT( zm)ZMh(v2XLl2v7^b}w?K_*KjDM&&f>i(KeHG+54Zd!}iBSl^*P*`c06%SaJw0#*UV z49UM9Lh$XT^AFDZ0lmgfoAk}N0shLA4qME@d@@){CrL7<>MJED5oHKsKV0(jGyIOl z4o@d68<>`AW7s%y*+CVJfvU)Rnd7Nj$txAymZ}wKdF|Nyjl9w5SZh z%k61JwZKtBH7dx^pTlxXD<>q3ExXHi=Ldt0t>6QWo-+}PuTGT0MZsi`(Rd}CFkE6? zwz?ob91vOSw`c+l;Bu{=F9&UD>P1=9`$OZXc0F0G5FVJcUNN$QSzIyx+ELuMczDK; zgla$IxAz=$rSZNsoUOUZ(Xn4hfhLUerWb1Cbv*Pv5gji+5{fv3d!bA5zWVNU%ltC~ z+QXvDq4{@~6(X4!=n^%9^J(}W=og##7YilQX?W@NQy*y3?gW$YbKRSA3Xn2{ zzd%&-6y3W{{NeTfwCa1yjn9IwJTOQ#y9Z$!dwhdZk>9(sC=w1Y`*VE8j{q8!)-MMumX$^0rY!7`x1(vXw1V58< z|C_!I718_#*;JEno ziLsf|6XLJe@-HoS^ekDnG<6TYEj`L4Ks2-SmMBw3jTfW@<8PmO2Wh#r)$pNM^E$BvqWxOdz5bGK+Sy0p z1>h6or1#JXiqtG*gHfh2UV8Pb$DeW!-h1r-pfzJ*#`U=Us%2kGoie&_sBt|k8jIX$UGKG-?~-%{SmZPs=x!QQ5t{DTao(pZM<&=nRY;jw+I^>KZq<`zf2Lq0X?4KIByu0 zW$m5jFWeZ(8?p3ye7o*&g;O6b`{@FQwdy!;x#PRP9YW(d8ZS4RfDg>u`~3|i^Yvrg z-tU5JQ&`^^zM%YfmSnp5%06p(Yu%)^#JVn4GM9b;5bRbt&j^uR!yRN-*~F_qm4pj zba|<9zuVlk`@#@Qner^!*I*N}{oVhaj&-L%)M zJo1DNJ8v`&6mYFLoWrTlxQov>k7~+fUAw>R4gL-+XMe9J$+j;|f|%;lW+@Bi>na%c z;9;B6BE+99eay&G9K0Uw;P&&KU$p1#H$F#4mtp2ar?zToDH&iGC?@4(4}6QZ%2gK~ zdEr>OtO$iwvVMPjnOOFc_$8t{F^^(k#D-GwHf%bzG8sPRlXUUWQ~B|W!gN+q<_re^ z`AjXep7(BD>NUIXA4JwAIE?b^_1%K1RVEko(VGw9F{cDU-0Za*?Pt_fzZ5OukMDNN z(v9rQEkv;Uc&0VnBQUBR#`iwRR23lwi~B6%K9J%AV~1Ad%B(KO9-k66_4t=TMwoW* zz(zJ7GE;>-#RXS?uBZ4o38Ly=0mTI0?+_mwBpy?Z+oXM|#|5sj+1tm^R;d`X!~0HQ4+S-iN}~F@T8>SwK_-Nt%rCfC_qR=M(wA@ zr>y}hvK7aVOA~=)Lj^)+=YOKmTja~`xaRUw=PEt%iM^S5eCxi6Uz(ngq5($`AAVk# zCB&Oa#IzMrJnzQ|CS#}OCDUEc^Kb^zxrFL3rRhRR8GQ<4IvcZKgfi*7(E}``Ur~BJ z{mT|y>8NhkBf%20bLh7Xh25ak$d{Ib?X(bh$)%S`;IX4Fl6lw9y-xWuuW> zLNm)Lf3Z?!I(?qRjhVS2L%?QVaISAm7V$oJ9owc=QkFKIZ^te$!NZnYAcU9=%qkp_ z&m{e*$iO=Z#L)D$W{jR54wxMGD^2O@03PBQbdwK^bc(9q`{rtei|R?H(6@r{H>yz? zXn%c9HW*d+`PO&6L4xN*Y^FQ(FB>|h&UI_baQeqHEc&%Bg4d@ST`HKr4IAlgFCbD^ z(23nvyCvF3eIm$Zkr_@{o%i~X2%8ot_G9q+9_|-QetyfNS1&AGKNn8{cPKqZu+LeL zjAd9r)T8>|uiqi4Lvay!Zz ziS7>^wn;s`?TcT`xD&BLpi&z84eFTbFr;fJAZj%JS%uAOqvdbAHV2W&w#pRCRuvk2 zS3~gFwrtA(8DX=I!#Sf@xTbXMbiSHwMVh?vUCRZZENQIGb39% za6nQ$O}OlQ286izGWq7&C>iFWilsc<(_nDRAIKX^hFL4$cNo{5G3D2)D4wHbgXYGa zWe<0bv@p9HS+e6RdTA+FgXwl?kWX8h5UB~edrKIMJSbUiF-qkk15bD^ z`o%$3(4f836W^#OXxY)6#`iSRea2|35aACK*_7su zQ`2b_?v~XuYVzj3^F zm`6YrO_dWD9eh8Se_YmeytJ-abbBZMRHC%D%nuOFW2021$BA6TT2Ql{5}MXe2i^E^ zD4!*D>Dq9?w?sjU-e@1HZ64U{~z;43Sw>K;Z>bQ(Ua zZ}=OjlaPnT6nEk10}_ z;0xl2do=@@;buo3-l}D{X7ySLvWF{7Q&T|>{$;toni=q^T)SL1b^zLX80r#1I1 z51;OVOQe@Zm^=~!9X9T->-87c?5W9GWICMAyu^5$O9hjivi3I+tCf z2VUM7L2G;(bL!#~5*KOx97_*}c2D_+OnIT6=}09~c|ug4Qp*-QjRc$WFMoVZ)H<%q zONkF2_LnVwmDDh{o!VBdz{}iRG+#wNr%m~W$@`e#YH-@VOcRcv*&FO5@{^1$z9CNS ziHVClrVRPhGpIi6Xd{;y;E`lD+Cu{N9rEX1@OpXM}Y)s|qqX zadF2znh8PVDh1JPNBGc;OJn|@B)ZCXZ#O}j!E(z&}rWlvHTIWHLOLg7^^G!-?|HR^;#PtqPA* z=CL-Q6=yew=N6U=Vp1Ab5QythhQ;bwiBh)S%MHDY6aK4&X(odXpvq0)tc;_1JSg)O zyt@n}N=|T|g0VVH%)zZNpYwx}9Mx2*SBoxXa7L|&)&L#2|1vK$pSHZ>qK1C_nPl9D zXn7B=5+0=Pf_k@}Qh781Z@o}_b;Hm1zbQPzU4Os>6(kM$_@aJrP3dz;9m?*Ls83*3 z45(Q_;A-o|ruWzK<=Q-7p1s3?nuV5(WKB=_lo=7TDdEoLc`gv-J9|u+W)_lJ4o20q z>FMP^--u;rO4(hrr2|d2D(WG2=We}uL~I5%1wUEB$IsAf->AC0^-TqOyTq>A6{nm2 zr@gz0FE!Orc9aTQO2V~HXYHM9^Xag^u|3U7Ty&iz+mntT(;TOG>6O#@1>gp~?e>ug zc@I9f(XvaXbBkS6HrbeH{S;>$pL1gE11P#)fL2<;HwI32+mxFKwTYV&ZO2HJ{S>z; zaFGfj(6FPqExu^Xf0KaIkhr@ zB+)-I2!&0i7dzdiD--%078l*tXYGHiiAU8<{21ZQSNB*7S|~#dhrS*%dq^^7dEZ)= zE`NI<%2_|2;gzeoGsz6(>QlqmV2iH_5?`VL`gBE}Y}ZM2sqn5^KN*zI`bX0}=Y?OK zESV;6uE0Js(-fE+)`b3OVjSi0 z=JLU>=xI!-HeOBi0u3uvJz*7z0TZl=&>Y^%~aGI}jT+8v;F`q$>>%QcS0S zE6n-(NY*!ZF;-Zo74GH?ti@1IfPYag{z39 z|B;p_E*@PYB{Qou+~8>h3-H4_*ys9H=SjjA))M>UsdFxGUc_WyG|7s{+Kv|c-x9KX z?0YuPN>EYY|G~!Muu4@*RZ?ynfkhk5MZZPhNvj*CIgm)a#mM)J}15^ZN zKQkbv0^wrpJ0Br>SHHscieBYBEktx+^=~5%E|52NeH8ArNSvK^tYEToY(@$-3Jo51 z#q!eALX18oi8`nNu%cuSIzSZj@hQ0uTEWk5qT=OjbFdXU8tlCM6Wj!lF;d0>Q_#wW zPCrf%AM`^TK-Mt3i`_%EIU& z*urC^PtiX=XYm3_N1X}5;K9!`;OD1K(`Jy*F`hw|dBQJ*Ef#xb)v;%|Q>JecF>x?(IncuZMxp%fZvk8vI6Sss`| zpHx>%zG}wL)ySb<>`h6|)h@c1-b`0vJ0{zcBuvxMl`IMowj2K#$t*#qCNF@vVqa?Nc$t;l zQeSEo4VTJ`FS5bQ3!NWaJ#Y@$<=+w4cuQr?BGdMfx^Q|~y z7y)(HUmT1WV*uKCuL59?DoK)Fu1`)K8qO6YJ@?b7?>u`^V%pYvPVLu+E=!xTk#O7O z>w0C5J5k)}F}Yb(5}>o$U@23#^viA9N9c9eN2&>+N0!?e9d3Z+7eXO34DY-!Z~G(_ zwF?bO_Npfp1sKmxiPDQL<+#Km_(khxH54|qheX^q4XQUPCZH)iTKP`^KpW>&9T;qj z-x%=*d$^ISJNhQ0%umJoSJeY%u{Uh3=3f99Zu%5&poxgp`)A2~$>i6kG&}7zj8s|69RQ_82-Q}R-x5Oil3iyKi#BqfB`D!f?b{G@&uQxL zYpnOd-c|kl$oX(xwGmae|Mz_$6&eKYBk z!se%-PIIgSaSFAIK5x@fb;nY%eOyZJbv2eD1}PIDd=MQ@}km*RR+G# zZyx^G){G&Epc4DUJka>EF~RF>r=v{`&yt9F@B)XupOKT%NC~BysuF+CFuf;P6!Sq< zihI!+ND;(-!UmSMw9A7)Ic);F{B^R%?fFAl2?~ek<@;oa`<*MRBKA{s_YA7orVKv4 z`Tc_dbmv>z+jJcwdofa^{pHFl|_bs`8(*0)MPow$>T(R@GrIZC4g8M6>;cWML z-c32#{L@n)*rBX?N1A7C^^f)f?$)S()Nu1v-zs;)XYG*Dm=2i6+9-gvu#lvm2Y1-_ zwSp)xGoq8uAC@_AP8RDYs~pXqO#dsIobily?duM-x)KGmZgN5Dk4>L^)EZ&ssa+=j zu<<+D0X5qaikYhO%4C{qloi1BkC-1U4 z2vX-N4y5{3UAu!vtP3?tK?r}VzQY2ab=&5m<}=2dJ<`#7yT9BY?L5>0##wMO8P3<; zaE9WdR8n{<9=yI=^gZ@`a6|0)p$M^Qn)Ohwsov>luT2oPIBT)5yiL$_Xk=$w1AEJR zB{P2gQI+v3986F5`icV_Ri z=E~Y>`y|4bZ7w3-FDn%;4ikZnu*LulwL4fHGS$NGmfPML6P@-M@f}d^Q1CPxKU$PF244Rl^Q;|Q{;_(U`YG2IaIze)!}1j8hA@uUmJ{C?!f zH2!7rrxn0_<%dP^)w4NMr=rZhV0NfFa5}Exkd-JenlV$Ay1&|%Jt1g@q(3+o^P;MFetst|E2cp=B&dnqE9E%o||J8ItKDK6?}!SWA@ty&ILOr_5$93CzDFF*dpcai4?b}yQrH<`O16U`Q;%M1Z)=xkjj(5XeMm|d z8QX45zqs`9n82-)QQ!U$^%~#9F$;Dchk~L?zq8{;n!Xp_@=^3m4^3^VqhOrJ2T_2q zd*?t(MfJG#h8=?PHOQ(wjZvSo?h9|AF4d35FO=ImEbu!}mDC4jAv$@;Ot#|#Ja47n zw7_PHn5yH=Z3U`h9%j5DvVzpPFnl0@QKb2OpS5>^HrvoGTC$2Al}?X*T}}!oBzW%zzHfU5U>7JUb-x{F<33HjJYhY>D7USA35ohC`j$Rq z(IwrxK2_BA3*)6T4fDgd5BXRTYlRw_JR4HZ0({DuscY!`@L&-W;|3M!CFE=KxHW{x z6U9oE-7!G9?+M{IY-<@(pHiKWSjDJvcFNNl8M3rT$^9hu1v??(J zjj7ozl!3TzaIy@T#K#-citsWAY@+5wkwoIqiFP}$>=PQ*@fH5d9aZZ?ce3${En>S# zuY@EK-%kqO+m53iN`nt0dr|YhQ?M()sqf@q$SyN!Z9Xjj>?A9DwPv3rcJTJW&%rYl zjlPlN{@?q(Xv*eY|GHC(ufulZA!9BYQOQ%#>FeHWcDbHw#?1p7r=4q|43mUx*i#oz zN#lG$y^-0Dt@DMRyVd1L{UrKR?kz6Jp18KduV?b_Ze+p(36a>J^%)keNb51WnJEZhUpnCb|Ocz^^5A+-^W6H z;hwoJjN0YWx_4a?$hTgCpI)#-`;tpn83GT#s%1zboznMps z%{M-fXz*r6tvTmqu(H2@^s37llq0uaBH!)%lYaV)Dz4AQXwY%LgC<+H?OC(*W^JB0 zzkVHf;U=@V3kuj)ge5aq6j)Xi+6e}%ul3ebc<#)DTNKsWx-_AaJQ}%5(>?l193A1A zrM~wRAHMy41MZ(4q1oO8$v4Y&(e6Ngn0{y&s_nVsCG9b}nl$BXLs{-a)F2q}EcPD9 zS&d=nQtNrovksB;%WaY$FVZgSPQ%qNt4=rt(H(kaUpMQp!DFwJ+T(Jz&JN{-R)hAi zb)itTc4s?#v!PCh#1+M9S-O`U+SLbN>l9UxcZZ%AxzYon)H_-;&Bmj(uJC~Ee)&6Z zEpdDJ#1U)x(N0=Ngpp)an0RQ6otQMs?NC)?Y6@P(uILcPTs@=Lm!csr3dUWm40lyP zj$U(}TKfh|_r~{hTZIIBc_;4s-LEs<&@R-As814%qDZa2JHC@>ArTCoCx{hI6zhY%&AJU|zSNR|f{PE`UONm2XPnb8 z!Y%pLQm$B3$?H{#hI6V2ad{`ou&?1O<4R_lOO-$NGaSG+%9`o(k8QcO#7(j+y5p3^ za9PILP+AY#tTQeU?*1IYu_cgy>g?^WI zaYi;9yLFY(ffW@4q2 z^sK?7Ll(yNs0iU2ilci{)#NvV-Sw&6iwuwf3(dG)W`o5 zN;f(sZ@(8n*$BptobU^tFYBY{Hb~TCcdKPa^Y zpt_S^!i?RHT%_UaLv&%r=!z!;%KP(2@ZHtb^lzC&z~<@8`3u^OqEmnU%jHwmdejr| z_Jw36hkjMV>Eo4uMtS9|2_~4iX8Wg|nSz+8`d~isUv`sh!#G`QcqN9}FJ_D9by_`n zAFXTFgo$_bkEs}*rbwlnxFG!8ONTJ~sqxhzi<(g{H{*6qd1QLb9snEdS0Zn^_H{M3 znnGJe?8m9TIf+k+?f3=$eCRlvo6z;<){UEk?$~dKJ6g+Gkw#&mqg9=l){o&jm?I6F z)7=`@mR*mYyft9>JZaplC-!dxId}E6C5qLnJn^hqh()PmL54+4OoJ;?{-rNi=oan7EB7GIzBN>O@K93U&QC7Bzr@ao+&@qp*dQf(Agoph> z+@^XO6f0htoh26+Pt5enyd&IOT3VQG2nvL}%MI7trN@k&%vNa+62<59qRp<~dZYxM z$uFu~;@9OK9@AN&;@@qoB$n|H{;k6rD*Vb?Jg9xaO0cbk!Xf|oxXFqw-L8a%=p}Rh zbkoWC-~(z?Jp!8$XBX|2k0XQ9$HF)sb~o^)&$^zuS?k9F(T0Vi>o+&o;Af+GJqz@x z9TV~TO>y=wAEwuqKLRuV$sxxbqtar0>qAPiRx(C}?wwy9My%VY6w0L*?WN&cy52Nw zMOLjziVvB6xJUXI1gkF|0NM4J4A*i3*MWj|p78u=BMQ0hM{0RXA636MLD`@Ep|?R< zM7_G#yjn!vBV}6PPt<&(i9|qDnuu0KsgSvgkE%*uy`#k_>Tuol#o4y=go{_OvgBz5635uP@XdKcoVlg+4+J6|)es!h9OGxg;p-VXfnG z`KdBK4%jtj_HpaX`Pm>#WZ}Ikm3+{9U+qD5xFV&4pBQ8qGs~oM4djrmlk2y~VI2VB zT69)t7JOGN5@SbYxFI10L8U&n?N(Q^w_9^og8a`;6L?oGo-NXVjiH82AK#@tw?C1n z07=H~&3aSS-FY5M$0~M#O|+1UyKTbwoHv?qn)P(^uCY|c8tVrKfxw{X=Ay@G=dBJE zQ*Ry60vwo8fdj+a=h5ThD`yk`nPPua$(8IAe9%d0&G~`B27t0x6jSxjqglj1LU1MQ z>d`zOsMh}ra^3^1ZGUwnL%j2i(toVGH?J%31qSn7xp)EZF2}tl*|>k627=;Ez}%=iL&pS1%p5Gh=DR(bq0m3(&_w!GE7_>_JpC`X zaCbikaE)09h3D~XD7SIFcfe=}0MLwIe}3e#z7Lcc4L|YuUYrFO*0|D&Kwd=gVxAoZ zV6s$^dvUzlkxq+U@@+`^tlvG!nD=*{59UWk_m#iyrmJ2-B`*UoP%Uyz%w_e4W`SO; z%I8NpoWV_N!Z7pJXN<)~M6(A5C6R!_wZg5eaD2XpC%t&S@H)tJ?|0Asaz>OIAdy;q zvVk-z|nF646UjTxG0gAqMj6sb6NHE(nV|1{Be|gI^_HRme zBiFR;osh>aOoChln)~a`#<=k6L?tAaNAnpJ^Or=@TZu&}nE+72xtqTd;Uw^r5D@?X zBcu=1Az9dlbLs`q&(8rMnQ7J$Id-yWbKfvSpZOdI&*F4{WgpEEg+R|sU+hqx0=w=( zTLAE+N78UAr9F$~A(_rLsuQ;xEpAfjcp&b!snqt4mPS^@5{)$;WSlsB-S?`K?Z8LIOfBXGN zK_LTfrO3+(_f`Z*1Es_OY+KkQkAA&vKH@h4LNu}&(iO+g>)n_)Ep`x->?;spIaYe- zU~NQIIJ8lyUdd;vk~RG$*Qe<%pFd0bTgD<4c=r?OW4cnrqr>$v^+!yg%+;y7n&K6| zWf~^gc5+~O-))re+|RBW!v^=hf=vK6yt^!%0LP=Iz%G1H9k&ALV_FBRuYppLQLTG_ zMfXhs81YbnzBY8A_;#9Rp4MRIb9xCww+jG-zWAh0NJxp&;?u~}iU1}AS1(dz9e^rN z%Cw{kHfDRm0-$!fP*OUi&zgn;p;25Ew>&j z?S{_Luev+#Nf>(2oE_Jt)}pFWqQAx!?9BE-Mx&2yk2WVKRJH(eo??IS#b63Koqhj= zD=>)oW4z>w$t~l*nP2_(QOuLBgu)&nBbUED_+}TF-|r%?mH5mw;tD6bx5xwu01n>P z&%!oZrvSj-@UguTi(OYUEyfNATtRYHsZYSM0^UVpycpr@Y5ERznzbq5YCrB*51ll> z$P!4rBW05E)-lKL8iC(pH z4lw%~JSdsPVOz#kZ)9p2^dEaUjF$ldF`1toht7E<1!Q7ODeDHO6_=KB-%hKeWPL)` z@S+1bvE#C+yuAi!rs^T#y4hh7YsPM$GROJ$XsPCk@lgAY;gbl`555A3Qj5NOMzG2M z(Gwa*OO@gKfev6~C09P}#}V>olqulvG__Wi_mt1MKNKZ;7pizag-vy*%a6FJ=XfP6 zmCnJDZsx1lXC-19vg*BS2~K`#1oh{K`d2(!Za}+0?@qdKLVa;0FOu=3s`@QjM4ee# zr^1%OW%;K}8E8tOBZ_rPOml&&C})aFW0#x);FCXN`-p+m`9-3ERr=5x*$RQ_-(KPT zlYd-X`o{*y9y%@jgy+ALdg0Sl5asN1*I4^D+-z4hq;ff{(BW)9Wg3o9NO8XSFG)1O zutEgvo|rdZPqInb|MX!|>y zQ%defkG7LDnrabFiR}1EIL_f4)_H0wnTe4TU$?A1(QRb>^2t6oLwg2S#BN*&Y|mDv zS81;-eQ}(Tq}=%2$WR(Bg3+&1pFoL@js*TZ2)$~M;7PF6qlCz!8j7Orxg)yT1z}*g z{L6=`6FCrX^BK$h#JdD3mm$KrTv_vNPwVe}m|i|bFQkUgcn;!TBh3CymCzplcZ>Ly zk%lvw+t5PLFgd5T*WsT=Qd;EGmZfVH)g9`MwlGK+Ird2&1B-xB6AtY5GP>Kg^R z-xO+RXJ;c_g6Ou}4uLX7+XOpZRR+t(L?!(i3IBCQ5hdvK{o=KTjpw{FGHk{NH zTNFMZhm$-uq9CLSmcZoJNf={d<8gF6ltmM^%mp{wHYy)RJfx30uAPvIK!?Ux%?sfJG%BSIKvX8L- zpORRB^dEJXgL_8Jib~(&_XVIrmcSbogwZjm-u?=J{*iXwywM?+j7GoDjw1@AlrB1n z@nlIG@ca*3!pSz3cf+Htyc19Fy@x-sOL6`m{lfr=vY3P!B4I#s2?Hf&K$C@V{dp`Q zr2C`Tw(+kXIffhmFFH-^{{t@!qA8t*oP~8=eE)RJz_6|l6nA&@9L!H2d^;BZqaIb1 z#K8LJpS`RGvIN)9-F>P$Z0ZtMI)2Qp0Xh_pK4LKU6x*@+x2-^-Hk`9T%TfX_o{gV;AfN0XiEuz@_$0$tU z#9ARGzVjmkjjZecf?eZ(8P^q)?24C`mK>@kXMW^p^i|j4Gu_a9?%-m4_xGV|F$KQm zYmZOJZltMYcK@7lQwaKapx$Rlr23*MAlUNg%Lnj5VBMQp_U4Pd5dJF)=2*|T`#Up~ zQ8bg8C^FapX|HebHjh!zXXUq<{rT6^XitOK>NBT@m7Y+{NP&RSnFo41Mx(@u_GK1n zEDy$Qj}C)=VAdu+{psP`ffP5*EC6nX4t|r%>eVQ-e88jjHG5PJOfI*ZO`(5XWKg~{F?nCB}c;xW@U`>!PGD9kJ-zBMBE3Wr*qPv4X+w;suK z>68d={1=~ct+tP@e%S-rsu`Nk)ULr?J79dVTX-$NWmWQ~+OUHp_=e{Z;|=~J9jnk0z{?I26=i@>7ajyj-$vs;x>q%6>q5EoyW^#@_z=D#a5>KCw^x13dS$r%C zG&*Czj5XZ0qc{Buz^MI>M;_S#jZX|^(VRw2dyRBA;?O@^?8R*wM?s);mKD^gh$Mh za=z6$RsXkkuVwEoEld(^oXc@Rh#6Tvl`2P4r+>~)E{}SmvLf(WE;8q{HnXUG$4G2n zertJGE0Ur$ICe9$Z*pAiwb1FbMiw-E+&4FMZMS>Ee!~E@p30o@6=mkdkYT@ME>L%$ zGibf}bzYl3gOos{(7+#xG&8u01JwHRQVRdJqVNEnCm zZ`zEjVb!|W(GcZ$Z42aW$!`PI!|L(yEaBve;PW~8+dlUu2EJaByF9zSI^M;vT#-01 zJ zbC`+T`(56lSKAS(53I#SKdb~l0K+Z;ZUdg%t_TU`L6zs1$pnUm35)O!bA~mEIdncN zvF~~Y(a0`-q8_yZb%{m;(c)fWQyJ|N=)#)KswOhvIhR4Gw3D81+aU+rElXq<2oW|t ze$l-0J5X3&M?XkdK?f4DclxKNf19D;PP<^P_V-2D9g8B7L3JbFPz39+tKJIA@zoWp z5#V*P>%S^O9C~H6Rpj)!EWXy)+i%EG34nd%CHIBpbW8@oh06(i)Yu`=bl;qC+ z<^-zJgyvke4Xp>7L6DrCmwvG!5Owf=F$<}&@YLamge5@WT*c|mmU`m*zU}3mEcjG< z&iG&U`AT!3+Kb~{8k6O?6(gBkJ-;3>Nm+*{P{=uJRj6W3D-dEcOghhzW)hR$+i^(5 zygqJrNlA8#H6}^LukPmQ)kFLa2Wa$)xbyXXiD$N~E27>RMNp z%VqNF$0vj3v2BBqfo4naOaQPY>0rX?r@m}OmSV0_n@Ge<%=bhkNH__YG0LFYI>jBG z=gKMgEygG13VM`0qbrp{q_#H?lEQUyq_G1#mlrf~p`Aaj!;CEedSd?QXMD6gT6$cg zFGer7BkK3}Voov)?{OA^>SdlG8qd@GclxSCf0}3HpqeCHsIRTQ7x>g4?rD&J=*yLr ztey0~3v!YFUZ9&0um4%fug*%h-T~@{|2j5J4LHO8Vi?D;1e((06}CU;se#v9{;czV zuuOOrR3m6Tj$eNnm298h9S_NSikGF5lAf!XH_-WkYv}9!tB)Tbd{l8$CHgW^p(%hM z=T%Ec8E~#gn2|#Pd}Q38E}sJw)Sb_c9RaUb;MU9VCsedxGQdmoN$Pm!+7RD;;3$Um zL`8x1Z~${+(-X$+QByIQLvrFY7D>++5x0cDGKK4e{PM*fZDn<^c!ld#*m5r-D7Piw zfk@IhVcJDT%9X#I$N-kZ-wI#sqUABF75fhqP#_?^kyh)mrv@C>TLBDEuJExK7Zv`A zH-&)HMlDAz^DY2_22sk4;=UKPACrNvci)+h;ZRfqz{6dDTxk4fs~-ng$*!tEUjdUA z2G|R42qg3`|N1TnoLE(YY8LbNIVH4TtKd>fcnsW|{_Fc&SJQXs)m zLM03)dvrV#DCBdxPsRA@v|kaZ_T*{iDFC!114$1VxN^IxXQ_bu^~U^%nsUZNt~1C2 zJ)W5&;r8w|S%F@8I$%3`@|_{P%%5JmeSaO;X16I<`d(gqO2oDr|W$JuH1M)K_rCf3TY$>S;^M8Zcuk@VX`QP@^#gdUvH8!R)aX_i>IxelR-JzC@^zp}Yz@ zk2IiWwF$sfj_we_H$>9$fz{HGkoy*^t6aX4HOsFq9JW9i#~+GvUDvpgTUC za9Fv(Zc#opF1Mxy0EeOyNJq5CcC2)4rIcgslU~IuKb?-^1b`QCiw4GsY|nQX*(JQI zab9^}7iBlfm(Zp7zSjgN&fyIBkTSaok?NxT6p*@{*W6$ zjyQ&`%?kKTsz-7cHTCtv>D5!HNvbQ}gPvj))!Jc#2Z@_3uX!>gxPSn30V72@_%2vP z?LX<5W#EZ+0OvX0Md=hr5+XZO2#JPuUKP*|kDK^hAD%+5w1kk;*pLEFoYrf=bc|)E z42&86(=PznB-7YmS%`n9hX(J_I~KHimS z38i#nVnRH5m4jCwP@q$ie$1n#(lHW#H%eFt{kDVI4S~4-ub-sz4ER?QeaI`zH9ti7 zegqL_XQv-p?|o8sax;uPMfJ+5WZ!V^3fRv_R=c_@%5FkX;&-T>?e1{AKaBbs-f6xn zI4Rksq2Q*XU=kBl|7EXuN>_UI-z=`uf7NX=-M=Pca{~eUAU^ovlk(2>{w(PshoAR< zY=)xVex+y4p7!%RV7_S{<QY9zpjicuNdu>775Nr z^zYJ|!}c*3s9KaWryvJVY*bi9$HeWa!LRD>fSPVzhp)I&#KLzKWTClIN%!ykxJSc2 z6T=y$c7aE*9Obe5;^3CB)j0jPjeVfdjqx>FefIj&Akyh~mpq_h#f0Hy4}C;^pN^KK zHzQg7Dp6nmgZp1aNoSD=?ro5;GXp|&fOKT$Y zQvr42G0q;AjH3>f>))`M6AuAP1S5%JB*XKbRCj?N_8`!Az|*SMAQNacUm;nxwajp8 zzA2lFee5GaS&@37EfI4BY%9y)-S@6cDAn`5H7$|!*`lSr2XBHB0a=PHPN{W| zs6JSIe&<H@Ym|Rt7HwuXDaoE8p@RGIh5L>eI>t`=T+Zm0sW#kx9Vm*IvyCCgH$mec_ zUT1wu%c~9QD0-12;3Mi#LN&#{n=F{MeGU30Ux2FrUtWq(@rqX;qH>1i?6kS0->xDp ze{Q&2pA-J$hmZr-;jgaDpEEKnibhvP?I-FpEYH4E z%}2R;zi^9*<{Gz3I1eZ)DttQ#W-i{`Q)#P6_QF5sJ0%14uxHXr@H;; zr=OKt?NVaC#MR>;yWn@foGiFsCtR#PA;3|tEj#iu9Y3bzAE}D78rc~vp&=|u{&9hx zvr-lek?FYonR1WJa8iOl_)5LjBEeT*K3yWH~V)inJ{autzp)W+JuCoI{ctgeyWM#9H4=xwp zPc_F5iA^Ut%IQt6;Ou&{F^>jG-$@e7VVggWZcss zw>oYug`|^{_y}hcu*vQb6SvZLg5~OoL5?l4siL!AW)PZ8-4CiGxAT@FozSq|;RX@;D{=Eya*>!P#b8GUG{0`4++rbt+ zhLh+J$(w}pOci!LT0lf{tfE5^{%GnQ=+}Fd!!l)eGnmR1_dcs#Oj zNE?Nkagum$%2_>#_m-(H=^B(Z5BQ_^g_o{Co)Lz6!lXrYk*R@>&7U%}JOWcW>|^b9 zRWxFhWJhlNwedLboxc0)HDr9)6@`TdK>A+kd;dpMt!;mGsZ|DY-9L;z(w-e-38b`D zb79%?<@Ai1QT$qGF`Lnc`i0H$`z?`YL(Jtul|A{*IU-E#Lc$U_Xa6;8vUY-YvGoi>X#hfCtHJl1HJZ+; zj*f*hT$F|`=BkTT*{GV|-dobpmUS_CPb&L7o5$`cVJ0m$mE?MB$hdAUj@iv7OMz8n z2n~P#@cvUWJTm7L=Z9CMhiOw~LJ4&_^+}iRE+R#9Nl7JkU(g=&C3Q|=E=33<>JVyM zKpRGo>*8=@w5>T(_H~W0-@@zk#(DXRxn_EGN;Q6|qly#yyrDjI@%_|$)sG$Zlu$r@ z&ttz|j_}WGLu;aooeW~T&O8#0pzB|O1~SqoAnwM@3Y&A-?ppMUmPLLYRWZ{q(k$P2 zRP3lIaY63qtr%m-+K<~WwBVETeHt$L#CVgv^V6KV?Hv%?6<1QR|L2C=`npE0go$K1qfdA%5R1MBUojqccQT zbvgS9j}1L{zI^@uBMpYz&+}T`c5M1&s)l0O-4-gf?-X%GiOTK2n+RbT@%{N(0`*XZ zQudG&A3R#=BEn;jTd|fN#0>DQN(`O0Fk>8I{$BrHUTR#O%~~8@qG`7_#ReO_caiM3 z9x(1Xj=Sbp)-YUU(#m>6krU!qRR?!vWzL<#lXzG^(VE5==;e$k2%cP|qbhz@_IKfF zCMwt73U3}8H|90Tn+V}{I$u_QTT8_xdk9^>c{fvcP7*1uYtrtk8JOSKas65Z!}!Xc z=3wux!!tz1BC&kdW$&fkPyvzH&0?wgixw~GoR)sx^=vOZ6&Z%eTTv=9foVwncyGuV za?Q7fLO78k8kkW1DqlVqzklVTRPN=IWvjXjMdK^0yesEaH*w5_lZ_+ZJq_z4MQ#i0 zQgjI~86LQ9_iOT;dg|a1VDrj8%ug^`iy%?;qCP^>MRD$7U>NZm-Ce2ifx~4t@|^ld zwl|+=c;6vjCNwg#HVG2wF~(f07)>3;eerOq(YUSz0WsmBE#&G3XTkr5$hp&MN@<4gqf# zwE;@s`~3^+?{n|(os!CL?Um^7?bUB{@6lhlHnksnuQ+zZXnP@?jkK|>P0uEw*M)i@ zt>X6<1el0}0d?^FT39?W+&gI$4E2oKVP_#$Nfx-vy3!#rjHxRwrQg+{By+Rj%hYMtuxB z4jY|}L&tSLus#?-H=@ce)#uJM)~=m&C7AV^V|Gel^WynS+tU#T!}K(hMU}2$t_PCD zp^KyO)S}3nd2Lr zu?kxiEumS=6+M;Ip4_)$N%~WsKsBG}0ly1y>{O=+Em=6yDwB9X|3sa<`LxpB{e2*S zyUyXy2++BP`BQmB!=T)eC;9y8?fdLnGyk4MO^saTeboQP_e zZac2}SEsZ|Ri92tXa4GxtQO7+8OI%F;Lz|VF1!V%@vk4B(S087bbjarj)V99EhbFZ z$@>L*FWl$4H365L2X55Una_}9N9j_a&~O|o0^L)`-`1zxuEI=DY+q#{nRt?Im8zOh zv;}k@k2AW&EbHF@fUEZ}CzgHF=d8g?J^Jx9i6rN{n(adSKtv}ytTYw3|6A@|P+?A{ z9#@2)eC8gKD9cnofGO78S0$v^Dk0($-Fp`i8Yt(ni9A$;Km0I)S0yqV_^86B0EUpv zBw{Z3RFbE0PeS_mkGMl&^WAV6U8)D_HajpqwdlRAE{x5PA}EP}^OCKC@hZQ1BWlwQ zp#MYI+PU;BU@8DT)6#I+fAmc3fA>tk=?4C;m!g*EMR{%)JaYzyGsSfpt*?37oLA$& z#Th(vF4Qk(1q)NVD88A73LgwqyO_9`pM5w%$1MDGIJyxq$)11~$4FSWo-8n_5nddt z)s?k2sd7{@LwX(+u;PAAGORwqF8vi`{M_pMd9#B~q@Jrvi2hv1fiDCxd z7f;tP9b-{YlOgfQiSq;X4LpFifHX$>Y(PJBK&P^#>#x6w+-yD3pJO;$1~fYu{X%`A zI*lj}IiSh#t8|-`ZyK#~(glhW088H@up?khn(x^SiaAT%c6TUGhL^_cYTy%v1|l-! z$ejUF3Nxpg=lp2blQdgLbty!?$o(5hmZtBU{_~nF0K4BppzMPg+Se%t&$?~*_n!*8 zV)U>hTvB+vIS_~nxT}wyy>!>zy}Ti4^QXlnqw?&AWiS&F%A9@HF5k;MNCD`8C1()h zoeG>)0qtXaQ|g=3cR9z-P<=y>C}Gj{n?J(2?+J+bS|{O(KNf3_jne+?JIq2)@Eb-W zcqanQ0E@&a>+^L&MNSj;c+Hi{`m3B#iIk|T%BfoP3NYKU|FHPpkur_P0B=$&!NQ~g zR%3-9HGM`s$2a48?G8*DE&_7|Od?1&%u`Pk&YwN$!@>z`F}+|@Um0TJTjTX0TR}(2 z`u_Et)YTH>s+WM>=l!@{8ZuID(?5GZn0Bt(1#`=}b(g%{7&`ndY^Y2K@Zf`5+Ae|C zY&?qK?)}*a+o^awmOF2%*m-+%#WHXlyaSZpPht%({oLOP^4D%TBewzW z!K`~o>nF8`_}giO0_O#sM)@53hBhkgdv9H<`$6>vsePu;m37@KjK#_)R=h3)=q2RU z^ZqKVGc2H4Tc!Zeu^*>f9t%WakCddRo}b& z+@+aQTvm^>`)X6(3vcB$Nnuk$ezM%2%*qCOlinW2~QOUDYn}a>w-)Sl;a7}i;lVm7H}pePepB$khHrYTMg#ig9x{P#t18x8CJPCB{R#Qeq` znNM%I|G!?8q)UNR@?QKhAnz{?iVFi}FL--D&JhTEl@;{U=>S*t%8v^)bI<0L$UmHj zb{T;Z)^`q6uk7dQI~m9Z65|R2yQXg+Z0O=i^i)fV|>gEYG`Lw?&Y zU#IDE*Hza5A)-Z`dzF(xl$^PiBbLl^6SROjDv_0GiL36c)j~`HI&sFbgDGAdvK;z+ zte%CZ#dcU6c9o60)wf=)5?*DRm|V$Hsqymq7wD50AX55ncIB3?lrE_qnu`4&u%+fz zD){w517fSj?d0M%QJVoYn6EgTdPR#g#OL+-ZMVhxb|R>Uz%qkpQL3qapd978xR3x0 z4^-`AeYbs8D)zc7wouX)u)LJR^tlljBGo1{pIS?ozeGc5Ui~67J{p|7qY)qdnK&QC zfA`tT_&*GQ5OU6J&|w5eZ{fRQkehO1r{03@n&YTOecO1nq=?+udx6F44>eK^a{8|} z75g*j0*PpI`+~I3lVePRic|!r0>Gq-!<2zLatKEm-xXFUelSrjPEcuUbuKT@xaKVY z?Ly-%zML4`i7}jw#O$;ljfWu( zZ6ecZHU0(Mocj~FX*itc1$9)nUB?cg9E0d#b_e4xnJI4?*PZC;1umQgPv;t!$$OiI zfFgdT;7bV1+nM#xOa8kB`K0a%NbZjdq77pr+Y=#s6ZjzaT$Jhw0N>y{d!ZO z94RdSU|zjzOMz(>Yv46I4t9BH43R@h^KSsBZ&4OQWM_PGUx;R>(2N#0?shk<*kCev zePK-`%tZmJ(^EJ~YP|Z?aj--k|JwUl%=V|);`!QcvybgptU?`kzf=HfzuTno=55kN zhr0J)I08xG*rvk_l9|9~He8*u0YruM|K?J`#=FX~Ts>fp%%%i_9d*MCiTr#_z9I65fY-$D7fEcXW$uL@pPUuo8(8xit9 zF+vX2GC%32sO38jCi?zUR8BsxUTf~o1oUx=+{CTBLNq_! z#+Ja!7ZX;`+5_j4KWA@6s(Re<%gwLY3n*+osFJLu`aQSEd*yy8uV~G7q9##3lPPSX zwMDO|&^&}H4ovRNGm$BzT-?}Ly~cK%#D*zQc+YM!7vji)@=o~&#?k?}DXJFZpLgzm z#aasH%EB%lh*H=(<1!f}Ui9iS5YWHbLs&R9lTMX?`Kk@~B*eO$JLo?Cp~$G>Idzp% z))D>H-LowSJ@CKhKcji25N*#LWVXIPa~byZg#sT)a<5(Fd8u2uK0=uaGvqU*=Hf=^ z4%ddo1^LzuEwO(>YQ&~lx+f`Xti-w-44Y@`Br9bq}0a);c~Z27U=)^Gdr z-M$Ij&zq0?%k=!Z<`e}_1X;~LEb~&SCg-0X>UqFUKh*zVoPKDo6&rGx;dF}%+qWip z!2QRl6w`@$%@3fZO=LIyF3epUF<%%JvhHHbbP;J79S8M<3h=nJy^>$qZ;*58z5~@) z;i<4m*jW2aD~@3B>F*9jH5tqgv5o-e<#D5_uxruo(gDp;|2&~Rg!U7H=}i!Ay_DV< z-M8D5JMY7Aa+}fcmjC(en8FL!W;^DRA}3RtO-|*$FLgcpu}=V@hS*BG4iN3D#gFHU zoHum&>y3Ca!$rFyQL)5954w7DqJ9I zu51)Q2*8_Kj{yC6jI?|9jNtoth5wEucyYwl7*^Wfa~wxrV+ZLj(rWA9^bM4^x%yzQ zD$@?ttd`B_5SVECqWui=BV8&Y?wu-t3L_jVk;@|%t{B&O9MO*1e6X~TpK94wLeXcBy zRD^b?N!?U{|D&cnZkA`rs%Z}}-&Y!c2#I>H-82R$EvpEgirB(}nrDCuVII9e&d zF5?A?^dTPIFumHFLVNR|Vd;Y~`s+X=eD;!F7;u8H*zREk;{J36ER^85qYP zmrSaQezGA@%L9mjDDh^e-M{84AEPBSBE#`Sa3cB1q`xM0Or)6%`azq|R1PjW<2~-{ zH#6h|Kj}RI-E}!o4*VEW{-=8sms?I}jA!qhcsB6BOmUlZ7US4h2gUYrCNg+s7wmcu3V)gc2usWGE zW_a(|DT31N{#0Yd_(o&f25)7ZNh6MAwp6GZ5V0I%`wvvc=S3?n5<7=_Q$aN{?oE4r zzh+>ga_h1hp;oNVWj1QEYq%dZtYpbsI)OP&8mgutV))Yp1|AW;EM@w#2*OpTp`x<> z?SZo)XWTrPekSneC5k}IH6T|k8MFcmDn3^rRCG82z2STHSM>wW4iM~Q1C*f97D$Pb z4OQ&-2S&%mSubZr4Tcq^MKTL1pb&TiluF85HW8+2faDhh&I~19I=R3C@?yco#-iTH zpxSKJ(0~>S>CRo#RsO;~{?r)&ytfY@;>xpH*#NMElHNWy=qA?(Dm1YtxmS`ByxW3I zq=s_5D*$g!tq++iK>QPaO->XXJl?Ssbr|n_SEL9N(;rID#uph?-4qhIr=Q@R`aX~c zN;=u^0^#Zic#@dPnNHk$4{$HcFCAV<1WODJ=R>L?jY-guE0^4~wKliAOYA;q`tpbA z@cwD}4`+?e*wEgTxOqn8RK2Ak&AB+G+N;z2H_v2o*}JM;3JEp?<^+0uP=lzU^dpy)?m`p)3M27Vs^Q6^~-DZUv&r7q4x=$l7oXDoiySi z4ePHP(yN2zpSgC$xB$SQ^}Gt4_@fgoe^PsL!_i=Zm$HI17fkboL9wQ2gwXgrg2|qY zT5Z2Pn}U|lE>i?qN{i6#3Fdm@a^h^_ay*l^oKKD~L06U2m#GyujRQU%&KtjClKB_puHd|$`0@q(jDxTySCB~eS@hYkvf0I?A^+TW#HZ{O# z7Pxv(MQ;6$svT;AV`vfD9a+*}YH>SR)aE6e0n4kC>X&n<7BQDA3v=-6CDDt^RzKiGAJ zEBa*X0r2|`t{3N8Iiv-`s2?nsg9UlO$6=iVMvCC$c=o|^>gUg?>VhlTQ~AFC%5F** zg*&x}ekyV25se{b%CwJ(r;qY`iF67O38P*9`lgaG|vG_8!r=&Z!EU~o+=V;QA zl1lc&P48(&48g_4&*T{M)ht1EEX>*YIck>VFGR^tXcs_qZ@)YvbZO?YUfc<9uLSKm z7)!OQXy+&CpJGFzEl6{7+|r!y{V(;>TCzZb8KFOG?^|}RhqzJ{{#KX-iy!M<(Gs|i ztTZ*Qe=m;OV)EUcVzIz~w#r`C{lapDl{cdfdU+0vXBHm7w6pJHFHaNf3lw zu4BlE3o0JDfQa%}VVzj-FNMZh@C^lLlqEP8f_vo_>GS7m8}wF9Wn2O zjH^yyCX6{qKdv~8*5?E?`eiv!KyNH9_1)+2%XS-H=`S{@^PTBMWARIi#WUMOXfl03 zdw0z!x;9XdG7-4_X23#vkn3mNeYTH%aFUrn%!S!a=0cW+-?l_emDut%KcN$Te(#bq zA7U04yHLfICax|iUnO93vm5E(2HmbMYdCP#eZ!n3;t#1Orn>odZ`(LYhAuYePrUrf zm~W368~(}1A_L1b8Zu#-X*Y;A1D=n?j?sx?u^y+uxpJ7j34(BrRnO)fPq}%g2Rn3rIVw2Wz@`^E0t$$V8?I>1CanyWrUz&{`9h2a> z>RnGfv>Tks0vfgiQ-?Hejuw_=x-1VB)Y;>&=OFw|-aq^v?NL79;gMj_!C6eHA4MB+ zmaY4HvpmrW#fm5NY9p#&%n{3eDkRVwgY6>})$nTRUe-LcXyZEa&_@rf-kQO_Zhr7k$ z{-+({+EbWW959Bccx8;dk!zcjy=#*pVoGY)M>${oT)&C<62&IPd~P8=uVbAM-iHnu zK|@E|7q;j=_9^WCVCzR0p<#h$iMLlp0vsL=g+dxPa0)l`n>`EG?$PdIYyWwJCadP- zsWo=?_gjPOe=%fw39d4mPW~}r&L5y1u* zGnOa3m~*s(H=_^Q~Sj>dW`wg*U%mhyV(Y+qbezMA`Fsqh0x6XgJ~kn z$t163g}yRNU-H)~)SbAGar@3z_mU@3Kylo@P##!5#dAt_ubE{J0}F zsh)8m$Y$DJMw|b2;vYt$KX%r=+G!d{NhVhdgk{`nHrZt)yGbnuv-{7%?ecj&N3}=? z%Om3q@rsua1}O;}>13FlD+>;_-ldM|buw#cD0C@YdtHfQ+g7=~g~=AMAV+x?8JhFo z9?whEPxp``kv_|k{N|B2v3TiGgf`S^YaM-1&upkych1jm!9H-D5`JP-GHr%{-!P}} zOG1-42XCka9lC|@*|x<8NyISZc2=%YO5_@=J2Yi381o@eOM5-q-|J`Ugtb_2_2N?W zt}lMP)Ken^BD6CRFkIVz^(q#hYzM>P1Mc!#=CaJ+&~H?t4_ZXb5);?#UR;qQF0A=` z6dGkzqccRYgr8X!_yD#Ynlo?riCQjc)+VBo1Cc&IO;TdWV$Wz%lzu=83c zJLCDs&76OJF%u*BSVR2P@$eo7ThxJ%H&DzI{h79Nhbdld7*-TXEVI z)-)sT9MRGs2Qr%e0?9mKp1AMDEiU}c>NMKy6Qm>itZ81L5-%dQVACKr!F?Qx zZVRTN9W%1{J_mttJv6>Df! zihG_%G_=pn$aD!D4 zrWcxRB)hx4)|aKi;9btYM7g!|rBDGh)-eSr4!!aj*>ZZjx&=&)_F;O~3&7r9VMX6+ zeX6$P)QJ8aPyfe?cZ|4XKv0Fl*EhjugpBp5&DT#buO0O4*?@JmmEi}55z(v%g$4)-1azHPQn&OPTHOU+i)$yQ}_ z7<*qj)SqYoA6KT>KD0w1b$(Di3yf8jJ(5buns?i#N5^Q-I{lJV2v~W)TfYD zRhMf0>mL26Evd&%_;!f^qsVm1M&X=KBz^QT9Wd0mz$nj4YA&r0eWeanvJEaQprF<= z1opc(Hi3cotA`oCADtSF@eIhYjR7n2*|dmxy`gcXy@hM6Az4W8T{L`o41vD-LxA@n zXQO;{35r8oA@Z80+jm@R`7>dslxLH~+P2VjtEvvL3~j)83v36clVyF&;TfqrSPgY0 z2;s6W>|%1)}y{pFmu};L94nCK& z*5DbL(9PI4)uO9>L_T>GJ%w#4%cVbeh08j!2*xl^KXYgh|xhqf*NXt-fDZ|tv&cj5s-}e zA%-m=8%vdvB;6PS|LTIs(pJZf-G*mnX@8V(9{&2O+IarLI(R+#CCq?`YKgVelfHB8 z-U%RWyE&j^U`oHlbmV+5Yz3C@)raNFlK} z#(m~;>ryj+^N?=M^!YeyKbGpvex$>L4KMY;3%@j(3_4Zk>NFJz+`?pzIz-sT)d_@| zBj`;S#QGIWiWRFhzYxP-uo`gT2MX8xa(qWt=UlQe4I8PFYV`8ERkpG9@=NRvKKz)_DP!EyPOaK~0i%Mr?Z!#3L^wHWwB=EO!y3wn2MwI})gDWBJu$krXPj3W^;6N(ei&50 znNESHLv4}b(b|UF{3E4Shz8W!`R(qj#w=I;*YUKzUs@63JM5#PjS1c#?@XX@!}FSt{V zeVL{0?xdg*6G8Thm}gBJu|Y+nvCLcrpEX2`88z2b3ZlM=`o>{eR{7+_-PIJm24o6L zMB{E#Xy;sR{N9rjk;I%U7!5>AX&RfD3vE)KAM3X~QFY<)jWvmyJ9*uW;xz^yCpZ`| zQPS(5E&3{b)wf^-WwDFjmW0h_-l~xNI6BIe3WsKsy2S%|>$9fdL_88bnCMV3n>6M4 zc%?4A;kL5VIA`aQ6}7g7jl6{@sH#Asayape-`xyKBsrx+(m5_AGKmP+j8B-OOsbl+ z>Sl&=i4xMs&_zm@jcP4YSB7%CW?t+M$w~laq0(ux_mwn$2MqkitdjxZbI2qkX^d@u zH)YjlH+Yp(@>+SnB{r*fRQ@bw5TUWvW{=hvgd)(Czc7cBGLwk(EOxf5A{s_qCT}e- z`mV$?mRnMWN(t4AQqJTdO= zWw{zxADS#Fh2XW3HDIjQ4XEwN zRBbi2)QyW6B4wwMd!;TS=@=EiEB;rhqt&&2UW~xG&^0_ndM?_cVob24=QA)5Ajo<% zSC)ob2jP`?vI5CBUJ6g{xi?9mN- zMbFYa2^2k@$#c6KSy((i^${QM8=_0DxjP=Ww0H%^%9; zO!q*UTFkeHehz8~1m4|l2UzM+pPUBT${Je`H-22^{$GX7q;oV+p3EYz2-=Ns`I(_Erhws!D zPr+ll+aCa~vuw72%E+m}ry+M)JV49*I>^yi8>8T7`0 zb1nQ3b##>?nK8h~8`2D&#~VhiSJ#c)14G!l$|0PDQTf5WXY)=mv&c1(y(;*1gO6ep zLWZHmrqREO;K{W zZ|SLA>!@6v<}Ytps-SG|E06IhF=>QSR`>VaG!0$=p6BWI%ZNCzW5n}p?}g#3(*)o) z%?3QHEyX7Z$l@!VM|55?iwFa9Cf0Qvc}Ght60Ts^ecquzH^S6w#D;GdMP7;plJmBd zYfjmKoUoWFzYaJ*QxFE`4(LN~edXxR)iveV^Yi#`e5nW_Imr$XjBW~;Z+@_82^4Xd z-Y@GSJO8kwEQChE>b<3jaLA(tLlo{vTb3FZo5$EMq1=I zM$J-*T(CVZw)-WL9n8k;t$+nLqWWdYgi42g4gNs}vv4p(bbzoq=L9&rVInZcbD_=s zi_ZYdJgcOuAfVYR*2cGgHZ#~ZsM_y)!>j+-)aVXIi)bj=X5%7Y>*BzSkcv*(?G)=# z1@m@}UcOFWgTJ>F2)S!OB|KUfF0)NNaOhZ)IJ=zFKWEkBMw7Qx+Bkft83h0(ZVDX) zB~{9uX>r?1s4=n?jN@iHNKXfWdznu?aRUMuzwNk ztT*OcupJympa*7ab7jgM?2&^7Yp3-{ zS{;fyxZ-#kZ$q@rsI}cGun5vP;jG{}&kHYY-M8;d&mF~^dUwU>Z>zLPIuAd{1Ea(M z*m+B|_C;P-eRU60+FY(T$Key_{{RUO)cUR7ixWP(xM+~Otyqa->G4N-i~~D-yQwxw z-p)+B?7SUV5Pg8MBM0^-=lyxDE`fDkS`WHuTO$S2#vPIqSrZTZ60u;LlMNzriQy#( zg&09Zfh}-WizjcPb)k_zz)c(*c@9*-DKXh#Kbj$}e!AAJ*~0mNCa?w0FakV(E>VU8 z(jsLb6_V&Aw?xf(K&$1b#an~?#&Uo}M0z zx(rN9B(EurwL-c#2$?&!5bvqMD3FHf&^WY6A+o3^kA3R5HOJV|=_^zYh=}hExrv#B z@Ko!{8c6c$=_R7ah7(<*v`0xHI+E=7(u@B38i)VFRVvzIm(E5ya6zr)fDz@!ASTKW ze^WvBYy*9M%?i*-T%_*t(%_m+cgZ*<6K>MTD6R5VaQ(Ka%nlWgGMY^YnZ2A5u=+wA z1PEM!$TX6lpnap5NTYH_W^OY$`w;nqZ)+rMyVD%zq&g5avYr<@ZY5sp`iM6jcYw`6 z>V3+R)$?n#i&mL0>9ux8<)J9J1Kdgmhh3D-PkuDi!9L3jn*Blz)w>Z2h8S zcctF1^D>9pFlxi4&;(?VwQ=2r%lQwDDZkKbAt`51nhLG66@%tN z_!|!ZmM>HwTKc1`G}kW2P7=P=1UYd$lK3IN0ovrGYIJS6nkz}C0JHZrBar1`dEH|! zvuSuF*h?iTDr=rZ%7IK`#>hD$H$>Z@Tx3u`lJ2E?Px!Xfa{vv4sUJ2+6zKD#2|uuJ zq_HM$SvX0BhN^rIyR@~#P!0`HNt*dy%k(llfL|z?>8-u)S{Wd{tg`Wz(-GuUh%kx_Y^Eh)1Ge2w@fw_TE$Q|~Bms}H6knhG^YXQ=%sHN=d6jqzAkC`ieS~_}gvP;nPeJS?aOBh=pdTFx%RbB{q zW#6aD>pfC$%;B)wC$KdVb4YXC=}D~#7}=#3ph*iKnqSC|zw_7!)ryceD~#=fYrpl6 zzvotleV3M??k0v;gwoBCNZ%)cS8452C6$I3Gba<(!u5Ap#B7t?GK|rz^YgWe1y<}a zeV87bs3XiI6B$!CjF09krvh+Zav6-ST~~aLN}Oa=(1{!`srukjz7Spv{dNKk2MF**A(8 zWZZ9^I4>ta9^{01PBIdn7LEEU9@ZSc)S!&$fL^Jy>)~JlsVcHt)3KtUvX^e_ImNub zLF8B1x?^aJyg)se$IeY6(Tl5z&)las9ky)O9E{%2yb>JqQ)Y6O=Y_aPUtAl4$7vgI z#K+nx<|5>UL*yrThQB0#5BPXUwx*f z>q0%l)^|c#Nym^apkCPPpDeA?&QT>g% z)eCvD6~rg%pc+oL!fGh50_rG5M;)16q=X=@*DqF}&nIfuMWqNFR!FkkJ-Wp6l6TJ7Z`I@(U%n z@EhG@pl4=GNQ{l;1-wK&Xrx1DF5aeIC-Ww!>|P8A<8J#9Op^ns0G)E{J6M4BOIgej zfz>k(7zI_SlP!@gYL8lq7E+|Mu3456tce);BhYRsekrIUCcdt_jl%q7SOJF8=18hk znJt#PIO&W$*SQn8_-HGD@JPyMy>blXl9vWv%#8dX4`g|^m0lkn?=J^^Tq%?X8_CZ5 z8O9_)Z#~|T08(DlKrz<$4%Q0*IT@S>V zm~WJJ7t_N< zq6ekQd(TsehHXRk`ZOSi9ft8>nLiaUJ)$jlh5N!f5&+Q^cTqiI7=cDWsDa(bMr=WT zwgqNJd5>;uW#TzhJ}y6ek)klp-{@x-8c<6LE2=;FsRa2ZvR#Y3^M0E&4qx3I7+)=v ztWFg;EGkxC*-Q{Fj>$ahaa>Jk${1X+ew3rNrEk5U<7w>dc!NBt1$uIzs&4|mBoYh*H z#@Vvb(C$Z7Mfb9v3j!uWaNLQkc=6YB%B{my=-MH5p)1||;znb`Z5Sa@sNA_l3QPKO z!uMx`S)a?g#FqP_fRP_}7-jAdv#UHWo$$eOeyxnyS?wIg^8GT-_v0{dPYm~Hjx(%zfLny&YN zQPto#x9N1o)EQ!;WQRyzM)ZlSYPB`y8nqUNs1A6));Sr?hUlFj4jHI^iYya(`#uyr zWaS{93d3yf)0D3KbHpy=*|d>kDSkqmhkC|?v~?g_iSc#r?d~A3t5uh&5!@E|ereX$ z=*=-I(#l1DKjqcuu0yI96}TCK9%+3+F!_4Kw+_eiSP5?{;6Cmm%zic+guOBqcHd1C zb6@|&vgH7br{EzEri2{131)s!B+Cyq-l)cM&b8$>v`jbE&P@k5W?XA>jFPg8*f94A zoSf`Y-59Z(<_MG{7j0dJMEC`ARA<?V9e}-@X-p08)R+-{Gw|g<|Tk{&j(K5!##T7YOT3&*%suCJSxU|BR?7 zk047a)<~vbeDi+rChrn4{-xSGc<{hT_&@$U9li(RUp4V;k>0xJJ(g1S3(U7oTzU`Q z(^PJo=B8!AZ=Wbj&o8EctN$nv#oexH94KmMEk@2EBPJ6w(as~TR6R-zWj z8~=?$0FcWkLBh}P&E;U5R-@7f!2v6unUh)^8=NRkGVd{b_Z%>rnl}DimZS^ON}W3B zViSHa&&PsSr%|HhG`I1x@$Tq}rRqC+qY^Nk5st7Vd2j?3dhEf{z*)amaenn&agYnG zH|D}6msA;@!GC>01N<0P1=BPx@j?wVo32+$%af~3J(Tzd{9>v~-xUAx$!q-~@Qm$G zFihL^G#mBGwEQD!q75!$$$$Sh>Q^YzeA2tcsznY6YZ2s{y2Rx+iF-Lgq4_#;DMY?#v%tiDq8O)X`-1(`oHi z02YlE+Op@{Gbn0g0NH_&qv#X%a4Pl#T=COjpGdEwm~$@%%fHmny&uj_5?clkPjql5bp9 z5ci8|cYVS`H@;JaHWv%djGUJT`}c7-`s2oS0qQjUKXB|ayMP_#i!K5C62=Lc3@xBP zBF>v@WXPNCF;xNmnv!jxS-7>jrptv_#J>lVFwtqw4@g@RfI1*a{%)QYdotKsy45dB zIpQ_YWG8R`4}gfc>zEXoN=FC%?^h-S4j#=8Y6ZQQrR+6YYSEkxc91Q9 zG^H%{@Y~~~k4eNur#U>4ib4RD0~^#A?90o&#RN7(NCe3Vtt(S4Gk_KEtMhgPo`6vJ z?m|V68qlS8LTmwamI&yy{_eHQq0BYoUa3?kvaL~w5M1K>g~r3>V6)(>kTZg5cmTC% zf%)_TzgF6n{SIJHgj+-X(gVa^5k^ql<3WaoI(QS>fCxJ;V-Og5ySgl_dfDE$miz2t za2PiB2Gdn_P36)lJ(^>4E_2|Ab^sh_5iKPprIVv}mR#jpVfpzOnE*`TIp6|uZ^e97 z;59C@R3a0up-ruJF2yp#W*%k*16iq!#)ItnU6L`4Mff%=74tDZ#w_gI=v88nuXB;* zs^wGXapa&@#aDtC6+4jkT1dLnFTxI(^DtNuIWi%U?e{fuNCw1?NCpj6fY=RD>JA!^ zkW=2JCV-Ncfj~@}GBZ2;!bg(b%6wKV9e{Nqnf1Y zRFr}%VoEU8LrOB6=b@TD2HDGKMEPj>-Bf$77j#Cw8mD`)2S6ytx~a%erQrsyHozr@_5J-0b9Fp&0eGTj*JcKx}iL= zHdR}GD?3@TFt`)EBNk;F-LLo7tJdTKA&n!f>BfsQ55W&SsMTg~az99g*rzTM4=~(` zfRhUo4KmJhba4GlJXY@npe~lhW(b~-j(%Z(JzTNKCR#33Qx-6V_mrL(tsRR6W|9XrD6)fL0HuwS1b getSelectedDatatypes() { throw new UnsupportedOperationException(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/comments/CommentsActionFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/comments/CommentsActionFactory.java index 1cbda515db..35f954ded2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/comments/CommentsActionFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/comments/CommentsActionFactory.java @@ -1,13 +1,12 @@ /* ### * 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. @@ -98,7 +97,7 @@ public class CommentsActionFactory { if (!isCommentSupported(loc)) { return false; } - return CommentType.isCommentAllowed(getCodeUnit(actionContext), loc); + return CommentTypeUtils.isCommentAllowed(getCodeUnit(actionContext), loc); } @Override @@ -132,7 +131,7 @@ public class CommentsActionFactory { @Override protected int getEditCommentType(ActionContext context) { CodeUnit cu = getCodeUnit(context); - return CommentType.getCommentType(cu, getLocationForContext(context), CodeUnit.NO_COMMENT); + return CommentTypeUtils.getCommentType(cu, getLocationForContext(context), CodeUnit.NO_COMMENT); } } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/comments/CommentsPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/comments/CommentsPlugin.java index 8a417c2670..7fe90c63c8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/comments/CommentsPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/comments/CommentsPlugin.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. @@ -129,7 +129,7 @@ public class CommentsPlugin extends Plugin implements OptionsChangeListener { * @param loc the {@link ProgramLocation} for which to delete the comment */ void deleteComments(Program program, ProgramLocation loc) { - int commentType = CommentType.getCommentType(null, loc, CodeUnit.EOL_COMMENT); + int commentType = CommentTypeUtils.getCommentType(null, loc, CodeUnit.EOL_COMMENT); SetCommentCmd cmd = new SetCommentCmd(loc.getByteAddress(), commentType, null); tool.execute(cmd, program); } @@ -138,7 +138,7 @@ public class CommentsPlugin extends Plugin implements OptionsChangeListener { if (codeUnit == null) { return false; } - int commentType = CommentType.getCommentType(null, loc, CodeUnit.NO_COMMENT); + int commentType = CommentTypeUtils.getCommentType(null, loc, CodeUnit.NO_COMMENT); return (commentType != CodeUnit.NO_COMMENT && codeUnit.getComment(commentType) != null); } @@ -215,7 +215,7 @@ public class CommentsPlugin extends Plugin implements OptionsChangeListener { else { historyAction.getPopupMenuData().setMenuPath(HISTORY_MENUPATH); } - historyAction.setEnabled(CommentType.isCommentAllowed(context.getCodeUnit(), loc)); + historyAction.setEnabled(CommentTypeUtils.isCommentAllowed(context.getCodeUnit(), loc)); return true; } }; @@ -227,7 +227,7 @@ public class CommentsPlugin extends Plugin implements OptionsChangeListener { private void showCommentHistory(ListingActionContext context) { CodeUnit cu = context.getCodeUnit(); ProgramLocation loc = context.getLocation(); - int commentType = CommentType.getCommentType(null, loc, CodeUnit.EOL_COMMENT); + int commentType = CommentTypeUtils.getCommentType(null, loc, CodeUnit.EOL_COMMENT); CommentHistoryDialog historyDialog = new CommentHistoryDialog(cu, commentType); tool.showDialog(historyDialog, context.getComponentProvider()); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorProvider.java index deb7e36560..368f4f3496 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/CompositeEditorProvider.java @@ -214,6 +214,12 @@ public abstract class CompositeEditorProvider extends ComponentProviderAdapter return new DefaultActionContext(this, null); } + public void selectField(String fieldName) { + if (fieldName != null) { + editorPanel.selectField(fieldName); + } + } + @Override public HelpLocation getHelpLocation() { return new HelpLocation(getHelpTopic(), getHelpName()); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorProvider.java index 63f1e25433..f80bbe596b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/compositeeditor/StructureEditorProvider.java @@ -97,12 +97,6 @@ public class StructureEditorProvider extends CompositeEditorProvider { return "DataTypeEditors"; } - public void selectField(String fieldName) { - if (fieldName != null) { - editorPanel.selectField(fieldName); - } - } - @Override protected void closeDependentEditors() { if (bitFieldEditor != null && bitFieldEditor.isVisible()) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypeManagerPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypeManagerPlugin.java index 4844c7cb35..75e3480668 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypeManagerPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypeManagerPlugin.java @@ -498,8 +498,8 @@ public class DataTypeManagerPlugin extends ProgramPlugin } @Override - public void edit(Structure dt, String fieldName) { - editorManager.edit(dt, fieldName); + public void edit(Composite composite, String fieldName) { + editorManager.edit(composite, fieldName); } @Override @@ -620,6 +620,15 @@ public class DataTypeManagerPlugin extends ProgramPlugin } } + @Override + public void setCategorySelected(Category category) { + if (provider.isVisible()) { + // this is a service method, ensure it is on the Swing thread, since it interacts with + // Swing components + Swing.runIfSwingOrRunLater(() -> provider.setCategorySelected(category)); + } + } + @Override public List getSelectedDatatypes() { if (provider.isVisible()) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesProvider.java index ef33490fbf..3f5bb0c064 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesProvider.java @@ -781,6 +781,45 @@ public class DataTypesProvider extends ComponentProviderAdapter { contextChanged(); } + /** + * Selects the given data type category in the tree of data types. This method will cause the + * data type tree to come to the front, scroll to the category and then to select the tree + * node that represents the category. If the category is null, the selection is cleared. + * + * @param category the category to select; may be null + */ + public void setCategorySelected(Category category) { + DataTypeArchiveGTree gTree = getGTree(); + if (category == null) { // clear the selection + gTree.clearSelectionPaths(); + return; + } + + DataTypeManager dataTypeManager = category.getDataTypeManager(); + if (dataTypeManager == null) { + return; + } + + ArchiveRootNode rootNode = (ArchiveRootNode) gTree.getViewRoot(); + ArchiveNode archiveNode = rootNode.getNodeForManager(dataTypeManager); + if (archiveNode == null) { + plugin.setStatus("Cannot find archive '" + dataTypeManager.getName() + "'. It may " + + "be filtered out of view or may have been closed (Data Type Manager)"); + return; + } + + // Note: passing 'true' here forces a load if needed. This could be slow for programs + // with many types. If this locks the UI, then put this work into a GTreeTask. + CategoryNode node = archiveNode.findCategoryNode(category, true); + if (node == null) { + return; + } + + gTree.setSelectedNode(node); + gTree.scrollPathToVisible(node.getTreePath()); + contextChanged(); + } + /** * Returns a list of all the data types selected in the data types tree * @return a list of all the data types selected in the data types tree diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/DataTypeEditorManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/DataTypeEditorManager.java index 5ccacdaf63..d405afe68c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/DataTypeEditorManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/editor/DataTypeEditorManager.java @@ -126,20 +126,24 @@ public class DataTypeEditorManager implements EditorListener { * Displays a data type editor for editing the given Structure. If the structure is already * being edited then it is brought to the front. Otherwise, a new editor is created and * displayed. - * @param structure the structure. + * @param composite the structure. * @param fieldName the optional name of the field to select in the editor. */ - public void edit(Structure structure, String fieldName) { + public void edit(Composite composite, String fieldName) { - StructureEditorProvider editor = (StructureEditorProvider) getEditor(structure); + CompositeEditorProvider editor = (CompositeEditorProvider) getEditor(composite); if (editor != null) { - reuseExistingEditor(structure); + reuseExistingEditor(composite); editor.selectField(fieldName); return; } - - editor = new StructureEditorProvider(plugin, structure, - showStructureNumbersInHex()); + if (composite instanceof Union) { + editor = new UnionEditorProvider(plugin, (Union) composite, showUnionNumbersInHex()); + } + else if (composite instanceof Structure) { + editor = new StructureEditorProvider(plugin, (Structure) composite, + showStructureNumbersInHex()); + } editor.selectField(fieldName); editor.addEditorListener(this); editorList.add(editor); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/memory/MemoryMapPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/memory/MemoryMapPlugin.java index ccde5fa0e6..3c2effe87a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/memory/MemoryMapPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/memory/MemoryMapPlugin.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. @@ -129,6 +129,11 @@ public class MemoryMapPlugin extends ProgramPlugin implements DomainObjectListen provider.setProgram(null); } + @Override + protected void locationChanged(ProgramLocation location) { + provider.locationChanged(location); + } + MemoryMapManager getMemoryMapManager() { return memManager; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/memory/MemoryMapProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/memory/MemoryMapProvider.java index f76b429a55..08fbcc3abd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/memory/MemoryMapProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/memory/MemoryMapProvider.java @@ -25,9 +25,9 @@ import javax.swing.table.TableColumn; import javax.swing.table.TableModel; import docking.ActionContext; -import docking.action.DockingAction; -import docking.action.ToolBarData; +import docking.action.*; import docking.action.builder.ActionBuilder; +import docking.action.builder.ToggleActionBuilder; import docking.widgets.OptionDialog; import docking.widgets.table.*; import docking.widgets.textfield.GValidatedTextField.MaxLengthField; @@ -40,12 +40,13 @@ import ghidra.program.model.address.Address; import ghidra.program.model.address.OverlayAddressSpace; import ghidra.program.model.listing.Program; import ghidra.program.model.mem.*; -import ghidra.util.HelpLocation; -import ghidra.util.Msg; +import ghidra.program.util.ProgramLocation; +import ghidra.util.*; import ghidra.util.exception.UsrException; import ghidra.util.table.GhidraTable; import ghidra.util.table.GhidraTableFilterPanel; import ghidra.util.table.actions.MakeProgramSelectionAction; +import resources.Icons; /** * Provider for the memory map Component. @@ -72,6 +73,9 @@ class MemoryMapProvider extends ComponentProviderAdapter { private Program program; private MemoryMapManager memManager; + private boolean followLocationChanges; + private ToggleDockingAction toggleNavigateAction; + MemoryMapProvider(MemoryMapPlugin plugin) { super(plugin.getTool(), "Memory Map", plugin.getName(), ProgramActionContext.class); this.plugin = plugin; @@ -104,6 +108,17 @@ class MemoryMapProvider extends ComponentProviderAdapter { return new ProgramActionContext(this, program, table); } + void locationChanged(ProgramLocation location) { + if (!followLocationChanges || location == null || location.getAddress() == null) { + return; + } + Memory memory = program.getMemory(); + MemoryBlock block = memory.getBlock(location.getAddress()); + if (block != null) { + filterPanel.setSelectedItem(block); + } + } + void setStatusText(String msg) { tool.setStatusInfo(msg); } @@ -320,6 +335,15 @@ class MemoryMapProvider extends ComponentProviderAdapter { MakeProgramSelectionAction action = new MakeProgramSelectionAction(plugin, table); action.getToolBarData().setToolBarGroup("B"); // the other actions are in group 'A' tool.addLocalAction(this, action); + + toggleNavigateAction = new ToggleActionBuilder("Memory Map Navigation", plugin.getName()) + .toolBarIcon(Icons.NAVIGATE_ON_INCOMING_EVENT_ICON) + .selected(false) + .helpLocation(new HelpLocation("MemoryMapPlugin", "Navigation")) + .description(HTMLUtilities.toHTML("Toggle on means to select the block" + + " that contains the current location")) + .onAction(c -> followLocationChanges = toggleNavigateAction.isSelected()) + .buildAndInstallLocal(this); } private boolean checkExclusiveAccess() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchtext/ListingDisplaySearcher.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchtext/ListingDisplaySearcher.java index e29cbf89e2..a0dfce9b27 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchtext/ListingDisplaySearcher.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/searchtext/ListingDisplaySearcher.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,6 +51,10 @@ import ghidra.util.task.TaskMonitor; * for that address. The textual representation also maintains information about the field * that generated it so that the search can be constrained to specific fields such as the * label or comment field. + * + *

    NOTE: This only searches defined instructions or data, which is possibly + * a mistake since this is more of a WYSIWYG search. However, searching undefined code units could + * make this slow search even more so. * */ class ListingDisplaySearcher implements Searcher { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin.java index a55242bbbe..0da369a65c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreePlugin.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. @@ -39,10 +39,11 @@ import ghidra.program.util.ProgramLocation; "in a tree hierarchy. All symbols (except for the global namespace symbol)" + " have a parent symbol. From the tree, symbols can be renamed, deleted, or " + "reorganized.", - eventsConsumed = { ProgramActivatedPluginEvent.class, ProgramLocationPluginEvent.class, ProgramClosedPluginEvent.class } + eventsConsumed = { ProgramActivatedPluginEvent.class, ProgramLocationPluginEvent.class, ProgramClosedPluginEvent.class }, + servicesProvided = { SymbolTreeService.class } ) //@formatter:on -public class SymbolTreePlugin extends Plugin { +public class SymbolTreePlugin extends Plugin implements SymbolTreeService { public static final String PLUGIN_NAME = "SymbolTreePlugin"; @@ -185,4 +186,10 @@ public class SymbolTreePlugin extends Plugin { tool.showComponentProvider(newProvider, true); return newProvider; } + + @Override + public void selectSymbol(Symbol symbol) { + connectedProvider.selectSymbol(symbol); + + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeProvider.java index 1ad5371cec..4f67780e9d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeProvider.java @@ -552,6 +552,10 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { return; } + selectSymbol(symbol); + } + + public void selectSymbol(Symbol symbol) { SymbolTreeRootNode rootNode = (SymbolTreeRootNode) tree.getViewRoot(); tree.runTask(new SearchTask(tree, rootNode, symbol)); } @@ -805,4 +809,5 @@ public class SymbolTreeProvider extends ComponentProviderAdapter { } } } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeService.java new file mode 100644 index 0000000000..47446e9f5d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/SymbolTreeService.java @@ -0,0 +1,29 @@ +/* ### + * 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.symboltree; + +import ghidra.program.model.symbol.Symbol; + +/** + * Service to interact with the Symbol Tree. + */ +public interface SymbolTreeService { + /** + * Selects the given symbol in the symbol tree. + * @param symbol the symbol to select in the symbol tree + */ + public void selectSymbol(Symbol symbol); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeManagerService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeManagerService.java index fd82185ffe..4b5ba09682 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeManagerService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeManagerService.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. @@ -96,14 +96,14 @@ public interface DataTypeManagerService extends DataTypeQueryService, DataTypeAr public void edit(DataType dt); /** - * Pop up an editor window for the given structure. + * Pop up an editor window for the given structure or union * - * @param structure the structure - * @param fieldName the optional structure field name to select in the editor window + * @param composite the structure or union + * @param fieldName the optional field name to select in the editor window * @throws IllegalArgumentException if the given has not been resolved by a DataTypeManager; * in other words, if {@link DataType#getDataTypeManager()} returns null */ - public void edit(Structure structure, String fieldName); + public void edit(Composite composite, String fieldName); /** * Selects the given data type in the display of data types. A null dataType @@ -113,6 +113,15 @@ public interface DataTypeManagerService extends DataTypeQueryService, DataTypeAr */ public void setDataTypeSelected(DataType dataType); + /** + * Selects the given data type category in the tree of data types. This method will cause the + * data type tree to come to the front, scroll to the category and then to select the tree + * node that represents the category. If the category is null, the selection is cleared. + * + * @param category the category to select; may be null + */ + public void setCategorySelected(Category category); + /** * Returns the list of data types that are currently selected in the data types tree * @return the list of data types that are currently selected in the data types tree diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/ProgramTreeService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/ProgramTreeService.java index e1256105c9..350a2ca8e0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/ProgramTreeService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/ProgramTreeService.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. @@ -28,30 +28,33 @@ import ghidra.program.util.GroupPath; * * */ -@ServiceInfo(defaultProvider = ProgramTreePlugin.class, description = "Get the currently viewed address set") +@ServiceInfo( + defaultProvider = ProgramTreePlugin.class, + description = "Get the currently viewed address set" +) public interface ProgramTreeService { - + /** * Get the name of the tree currently being viewed. */ public String getViewedTreeName(); - + /** * Set the current view to that of the given name. If treeName is not * a known view, then nothing happens. * @param treeName name of the view */ public void setViewedTree(String treeName); - + /** * Get the address set of the current view (what is currently being shown in * the Code Browser). */ - public AddressSet getView(); - + public AddressSet getView(); + /** * Set the selection to the given group paths. - * @param gps paths to select + * @param groupPaths paths to select */ - public void setGroupSelection(GroupPath[] gps); + public void setGroupSelection(GroupPath... groupPaths); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuckFixTableProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuckFixTableProvider.java new file mode 100644 index 0000000000..7829baee02 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuckFixTableProvider.java @@ -0,0 +1,356 @@ +/* ### + * 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.quickfix; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.*; +import javax.swing.table.TableModel; + +import docking.*; +import docking.action.ToggleDockingAction; +import docking.action.builder.ActionBuilder; +import docking.action.builder.ToggleActionBuilder; +import docking.widgets.table.GTable; +import docking.widgets.table.threaded.ThreadedTableModel; +import generic.theme.GIcon; +import ghidra.app.nav.Navigatable; +import ghidra.app.services.GoToService; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramTask; +import ghidra.util.HelpLocation; +import ghidra.util.table.*; +import ghidra.util.table.actions.DeleteTableRowAction; +import ghidra.util.table.actions.MakeProgramSelectionAction; +import ghidra.util.task.TaskLauncher; +import ghidra.util.task.TaskMonitor; + +/** + * Component Provider for displaying lists of {@link QuickFix}s and the actions to execute them + * in bulk or individually. + */ +public class QuckFixTableProvider extends ComponentProvider { + private static final Icon EXECUTE_ICON = new GIcon("icon.base.plugin.quickfix.done"); + private JComponent component; + private QuickFixTableModel tableModel; + private GhidraThreadedTablePanel threadedPanel; + private GhidraTableFilterPanel tableFilterPanel; + private GhidraTable table; + private ToggleDockingAction toggleAutoDeleteAction; + private boolean autoDelete; + + public QuckFixTableProvider(PluginTool tool, String title, String owner, Program program, + TableDataLoader loader) { + super(tool, title, owner); + setIcon(new GIcon("icon.plugin.table.service")); + setTransient(); + setTitle(title); + + tableModel = new QuickFixTableModel(program, title, tool, loader); + tableModel.addInitialLoadListener(b -> tableLoaded(b, loader)); + + component = buildMainPanel(); + + createActions(owner); + + tableModel.addTableModelListener(e -> tableDataChanged()); + } + + protected void tableLoaded(boolean wasCancelled, TableDataLoader loader) { + // used by subclasses + } + + private void updateSubTitle() { + StringBuilder builder = new StringBuilder(); + builder.append(" "); + int count = tableModel.getUnfilteredRowCount(); + if (count > 0) { + builder.append("("); + builder.append(count); + builder.append(count == 1 ? " item)" : " items)"); + } + setSubTitle(builder.toString()); + } + + protected void createActions(String owner) { + new ActionBuilder("Apply Action", owner) + .popupMenuPath("Apply Selected Items(s)") + .popupMenuIcon(EXECUTE_ICON) + .popupMenuGroup("aaa") + .toolBarIcon(EXECUTE_ICON) + .description("Applies the selected items") + .helpLocation(new HelpLocation("Search", "Apply_Selected")) + .keyBinding("ctrl e") + .withContext(QuickFixActionContext.class) + .enabledWhen(c -> c.getSelectedRowCount() > 0) + .onAction(this::applySelectedItems) + .buildAndInstallLocal(this); + + toggleAutoDeleteAction = new ToggleActionBuilder("Toggle Auto Delete", owner) + .popupMenuPath("Auto Delete Completed Items") + .popupMenuGroup("settings") + .helpLocation(new HelpLocation("Search", "Auto_Delete")) + .description("If on, automatically remove completed items from the list") + .onAction(this::toggleAutoDelete) + .buildAndInstallLocal(this); + + addLocalAction(new SelectionNavigationAction(owner, table)); + + GoToService service = dockingTool.getService(GoToService.class); + if (service != null) { + Navigatable navigatable = service.getDefaultNavigatable(); + addLocalAction(new MakeProgramSelectionAction(navigatable, owner, table, "bbb")); + } + DeleteTableRowAction deleteAction = new DeleteTableRowAction(table, owner, "bbb") { + @Override + public void actionPerformed(ActionContext context) { + super.actionPerformed(context); + updateSubTitle(); + } + }; + addLocalAction(deleteAction); + + } + + @Override + public ActionContext getActionContext(MouseEvent event) { + return new QuickFixActionContext(); + } + + private void tableDataChanged() { + updateTitle(); + } + + private void updateTitle() { + int rowCount = tableModel.getRowCount(); + int filteredRowCount = tableFilterPanel.getRowCount(); + setSubTitle("(" + filteredRowCount + " of " + rowCount + ")"); + } + + @Override + public void closeComponent() { + super.closeComponent(); + tableFilterPanel.dispose(); + } + + @Override + public JComponent getComponent() { + return component; + } + + protected JPanel buildMainPanel() { + JPanel panel = new JPanel(new BorderLayout()); + threadedPanel = new GhidraThreadedTablePanel<>(tableModel) { + protected GTable createTable(ThreadedTableModel model) { + return new QuickFixGhidraTable(model); + } + }; + table = threadedPanel.getTable(); + table.getSelectionModel().addListSelectionListener(e -> { + if (e.getValueIsAdjusting()) { + return; + } + dockingTool.contextChanged(QuckFixTableProvider.this); + }); + + table.setActionsEnabled(true); + table.installNavigation(dockingTool); + + panel.add(threadedPanel, BorderLayout.CENTER); + panel.add(createFilterFieldPanel(), BorderLayout.SOUTH); + panel.setPreferredSize(new Dimension(1000, 600)); + return panel; + } + + private JPanel createFilterFieldPanel() { + tableFilterPanel = new GhidraTableFilterPanel<>(table, tableModel); + return tableFilterPanel; + } + + public boolean isBusy(TableModel model) { + + if (!(model instanceof ThreadedTableModel)) { + return false; + } + + ThreadedTableModel threadedModel = (ThreadedTableModel) model; + if (threadedModel.isBusy()) { + return true; + } + return false; + } + + private void applySelectedItems(QuickFixActionContext context) { + List selectedItems = tableFilterPanel.getSelectedItems(); + int nextIndex = selectedItems.size() == 1 ? table.getSelectedRow() : -1; + + applyItems(selectedItems); + + if (nextIndex >= 0) { + int index = nextIndex + 1; + if (index < table.getRowCount()) { + table.selectRow(nextIndex + 1); + } + } + + if (autoDelete) { + removeCompletedItems(selectedItems); + } + tableModel.fireTableDataChanged(); + } + + private void toggleAutoDelete(ActionContext context) { + autoDelete = toggleAutoDeleteAction.isSelected(); + if (autoDelete) { + removeCompletedItems(tableModel.getModelData()); + } + } + + private void removeCompletedItems(List items) { + List toDelete = new ArrayList<>(); + for (QuickFix item : items) { + if (item.getStatus() == QuickFixStatus.DONE) { + toDelete.add(item); + } + } + + for (QuickFix quickFix : toDelete) { + tableModel.removeObject(quickFix); + } + } + + private void applyItems(List quickFixList) { + Program program = tableModel.getProgram(); + ProgramTask task = new ApplyItemsTask(program, getTaskTitle(), quickFixList); + TaskLauncher.launch(task); + } + + public void executeAll() { + List allItems = tableModel.getModelData(); + applyItems(allItems); + tableModel.fireTableDataChanged(); + } + + protected String getTaskTitle() { + return "Applying Items"; + } + + public void programClosed(Program program) { + if (program == tableModel.getProgram()) { + this.closeComponent(); + } + } + + private static class ApplyItemsTask extends ProgramTask { + + private List quickFixList; + + public ApplyItemsTask(Program program, String title, List quickFixList) { + super(program, title, true, true, true); + this.quickFixList = quickFixList; + } + + @Override + protected void doRun(TaskMonitor monitor) { + for (QuickFix quickFix : quickFixList) { + quickFix.performAction(); + } + } + + } + + /** + * Returns the table model. + * @return the table model + */ + public QuickFixTableModel getTableModel() { + return tableModel; + } + + /** + * Sets the selected rows in the table + * @param start the index of the first row to select + * @param end the index of the last row to select + */ + public void setSelection(int start, int end) { + table.setRowSelectionInterval(start, end); + } + + /** + * Returns the selected row in the table + * @return the selected row in the table + */ + public int getSelectedRow() { + return table.getSelectedRow(); + } + + /** + * Applies all the selected items. + */ + public void applySelected() { + applySelectedItems(null); + } + +//================================================================================================== +// Inner Classes +//================================================================================================== + private class QuickFixActionContext extends DefaultActionContext { + QuickFixActionContext() { + super(QuckFixTableProvider.this, table); + } + + public int getSelectedRowCount() { + return table.getSelectedRowCount(); + } + } + + private class QuickFixGhidraTable extends GhidraTable { + boolean fromSelectionChange = false; + + public QuickFixGhidraTable(ThreadedTableModel model) { + super(model); + } + + @Override + public void navigate(int row, int column) { + if (!doSpecialNavigate(row)) { + super.navigate(row, column); + } + } + + @Override + protected void navigateOnCurrentSelection(int row, int column) { + fromSelectionChange = true; + try { + super.navigateOnCurrentSelection(row, column); + } + finally { + fromSelectionChange = false; + } + } + + private boolean doSpecialNavigate(int row) { + QuickFix quickFix = tableFilterPanel.getRowObject(row); + return quickFix.navigateSpecial(dockingTool, fromSelectionChange); + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFix.java new file mode 100644 index 0000000000..8c5993f8df --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFix.java @@ -0,0 +1,231 @@ +/* ### + * 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.quickfix; + +import java.util.Map; + +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; + +/** + * Generic base class for executable items to be displayed in a table that can be executed in bulk or + * individually. + */ +public abstract class QuickFix { + private long modificationNumber; + private QuickFixStatus status = QuickFixStatus.NONE; + private String statusMessage; + protected final Program program; + protected final String original; + protected final String replacement; + protected String current; + + protected QuickFix(Program program, String original, String replacement) { + this.program = program; + this.original = original; + this.replacement = replacement; + this.current = original; + this.modificationNumber = program.getModificationNumber(); + } + + /** + * Returns the general name of the action to be performed. + * @return the general name of the action to be performed + */ + public abstract String getActionName(); + + /** + * Returns the type of program element being affected (function, label, comment, etc.) + * @return the type of program element being affected + */ + public abstract String getItemType(); + + /** + * Returns the address of the affected program element if applicable or null otherwise. + * @return the address of the affected program element if applicable or null otherwise + */ + public abstract Address getAddress(); + + /** + * Returns a path (the meaning of the path varies with the item type) associated with the + * affected program element if applicable or null otherwise. + * @return a path associated with the affected program if applicable or null otherwise + */ + public abstract String getPath(); + + /** + * Returns the original value of the affected program element. + * @return the original value of the affected program element. + */ + public String getOriginal() { + return original; + } + + /** + * Returns the current value of the affected program element. + * @return the current value of the affected program element. + */ + public final String getCurrent() { + refresh(); + return current; + } + + protected void refresh() { + if (program.getModificationNumber() == modificationNumber) { + return; + } + modificationNumber = program.getModificationNumber(); + + // once in an error status, it must stay that way (to distinguish it from the + // "not done" state, otherwise we would clear it when refresh the status) + if (status == QuickFixStatus.ERROR) { + return; + } + + current = doGetCurrent(); + updateStatus(); + } + + /** + * Returns a preview of what the affected element will be if this item is applied. + * @return a preview of what the affected element will be if this item is applied + */ + public final String getPreview() { + return replacement; + } + + /** + * Executes the primary action of this QuickFix. + */ + public final void performAction() { + if (status == QuickFixStatus.ERROR || status == QuickFixStatus.DONE) { + return; + } + execute(); + } + + /** + * Returns the current {@link QuickFixStatus} of this item. + * @return the current {@link QuickFixStatus} of this item + */ + public final QuickFixStatus getStatus() { + refresh(); + return status; + } + + /** + * Returns the current status message of this item. + * @return the current status message of this item + */ + public String getStatusMessage() { + if (statusMessage != null) { + return statusMessage; + } + switch (status) { + case DONE: + return "Applied"; + case ERROR: + return "Error"; + case NONE: + return "Not Applied"; + case WARNING: + return "Warning"; + case CHANGED: + return "Target changed externally"; + case DELETED: + return "Target no longer exists"; + default: + return ""; + } + } + + /** + * Sets the status of this item + * @param status the new {@link QuickFixStatus} for this item. + */ + public void setStatus(QuickFixStatus status) { + setStatus(status, null); + } + + /** + * Sets both the {@link QuickFixStatus} and the status message for this item. Typically, used + * to indicate a warning or error. + * @param status the new QuickFixStatus + * @param message the status message associated with the new status. + */ + public void setStatus(QuickFixStatus status, String message) { + this.status = status; + this.statusMessage = message; + } + + public abstract ProgramLocation getProgramLocation(); + + public Map getCustomToolTipData() { + return null; + // do nothing - for subclasses to put specific info into the tooltip + } + + /** + * Returns the current value of the item. + * @return the current value of the item + */ + protected abstract String doGetCurrent(); + + /** + * Executes the action. + */ + protected abstract void execute(); + + /** + * QuickFix items can override this method if they want to do some special navigation when the + * table selection changes or the user double clicks (or presses key) to navigate. + * @param services the tool service provider + * @param fromSelectionChange true if this call was caused by the table selection changing + * @return true if this item handles the navigation and false if the QuickFix did not + * handle the navigation and the client should attempt to do generic navigation. + */ + protected boolean navigateSpecial(ServiceProvider services, boolean fromSelectionChange) { + return false; + } + + private void updateStatus() { + QuickFixStatus newStatus = computeStatus(); + if (newStatus != status) { + setStatus(newStatus); + statusChanged(status); + } + } + + protected void statusChanged(QuickFixStatus newStatus) { + // do nothing - used by subclasses + } + + private QuickFixStatus computeStatus() { + if (current == null) { + return QuickFixStatus.DELETED; + } + if (current.equals(original)) { + return QuickFixStatus.NONE; + } + if (current.equals(replacement)) { + return QuickFixStatus.DONE; + } + return QuickFixStatus.CHANGED; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixStatus.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixStatus.java new file mode 100644 index 0000000000..c3e90daa73 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixStatus.java @@ -0,0 +1,28 @@ +/* ### + * 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.quickfix; + +/** + * Enum for the possible status values of a {@link QuickFix}. + */ +public enum QuickFixStatus { + NONE, // The item is unapplied and is ready to be executed + WARNING, // The item is unapplied and has an associated warning + CHANGED, // The item is unapplied, but has changed from its original value + DELETED, // The item's target program element no longer exists + ERROR, // The item can't be applied. This may occur before or after it is applied + DONE // The item has been applied +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixStatusRenderer.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixStatusRenderer.java new file mode 100644 index 0000000000..e975a82a75 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixStatusRenderer.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.quickfix; + +import java.awt.Component; + +import javax.swing.Icon; +import javax.swing.JLabel; + +import docking.widgets.table.GTableCellRenderingData; +import generic.theme.GIcon; +import ghidra.docking.settings.Settings; +import ghidra.util.exception.AssertException; +import ghidra.util.table.column.AbstractGhidraColumnRenderer; +import resources.Icons; + +/** + * Renderer for the {@link QuickFixStatus} column + */ +public class QuickFixStatusRenderer extends AbstractGhidraColumnRenderer { + + private static final Icon DONE_ICON = new GIcon("icon.base.plugin.quickfix.done"); + private static final Icon ERROR_ICON = Icons.ERROR_ICON; + private static final Icon WARNING_ICON = Icons.WARNING_ICON; + private static final Icon DELETE_ICON = Icons.DELETE_ICON; + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + + JLabel renderer = (JLabel) super.getTableCellRendererComponent(data); + QuickFixStatus status = (QuickFixStatus) data.getValue(); + + Icon icon = getIcon(status); + renderer.setIcon(icon); + renderer.setText(""); + QuickFix rowObject = (QuickFix) data.getRowObject(); + renderer.setToolTipText(rowObject.getStatusMessage()); + + return renderer; + } + + private Icon getIcon(QuickFixStatus status) { + switch (status) { + case DONE: + return DONE_ICON; + case WARNING: + case CHANGED: + return WARNING_ICON; + case ERROR: + return ERROR_ICON; + case DELETED: + return DELETE_ICON; + case NONE: + return null; + default: + throw new AssertException("Unexpected QuickFix status: " + status); + } + } + + @Override + public String getFilterString(QuickFixStatus t, Settings settings) { + return t.toString(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixTableModel.java new file mode 100644 index 0000000000..65a2568a82 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixTableModel.java @@ -0,0 +1,346 @@ +/* ### + * 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.quickfix; + +import java.awt.Component; +import java.util.Map; +import java.util.Map.Entry; + +import javax.swing.JLabel; + +import docking.widgets.table.*; +import ghidra.docking.settings.Settings; +import ghidra.framework.model.DomainObjectChangedEvent; +import ghidra.framework.model.DomainObjectListener; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.util.HTMLUtilities; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.table.GhidraProgramTableModel; +import ghidra.util.table.column.AbstractGhidraColumnRenderer; +import ghidra.util.table.column.GColumnRenderer; +import ghidra.util.task.SwingUpdateManager; +import ghidra.util.task.TaskMonitor; + +/** + * Table model for {@link QuickFix}s + */ +public class QuickFixTableModel extends GhidraProgramTableModel + implements DomainObjectListener { + private TableDataLoader tableLoader; + private SwingUpdateManager updateManager = new SwingUpdateManager(1000, this::refreshItems); + private QuickFixRenderer quickFixRenderer = new QuickFixRenderer(); + + protected QuickFixTableModel(Program program, String modelName, ServiceProvider serviceProvider, + TableDataLoader loader) { + super(modelName, serviceProvider, program, null); + this.tableLoader = loader; + + program.addListener(this); + } + + @Override + public void dispose() { + updateManager.dispose(); + if (program != null) { + program.removeListener(this); + } + program = null; + super.dispose(); + } + + @Override + protected void doLoad(Accumulator accumulator, TaskMonitor monitor) + throws CancelledException { + tableLoader.loadData(accumulator, monitor); + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + + descriptor.addVisibleColumn(new OriginalValueColumn(), 1, true); + descriptor.addHiddenColumn(new CurrentValueColumn()); + descriptor.addVisibleColumn(new PreviewColumn()); + descriptor.addVisibleColumn(new ActionColumn()); + descriptor.addVisibleColumn(new TypeColumn()); + descriptor.addHiddenColumn(new AddressColumn()); + descriptor.addHiddenColumn(new PathColumn()); + descriptor.addVisibleColumn(new QuickFixStatusColumn()); + + return descriptor; + } + + @Override + public ProgramLocation getProgramLocation(int modelRow, int modelColumn) { + if (modelRow < 0 || modelRow >= filteredData.size()) { + return null; + } + + QuickFix quickFix = filteredData.get(modelRow); + return quickFix.getProgramLocation(); + } + + @Override + public void domainObjectChanged(DomainObjectChangedEvent ev) { + updateManager.update(); + } + + private void refreshItems() { + fireTableDataChanged(); + } + + private class QuickFixStatusColumn + extends AbstractDynamicTableColumnStub { + + QuickFixStatusRenderer renderer = new QuickFixStatusRenderer(); + + @Override + public String getColumnName() { + return "Status"; + } + + @Override + public int getColumnPreferredWidth() { + return 25; + } + + @Override + public QuickFixStatus getValue(QuickFix rowObject, Settings settings, + ServiceProvider sp) throws IllegalArgumentException { + return rowObject.getStatus(); + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + } + + private class TypeColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getColumnName() { + return "Type"; + } + + @Override + public int getColumnPreferredWidth() { + return 100; + } + + @Override + public String getValue(QuickFix rowObject, Settings settings, + ServiceProvider sp) throws IllegalArgumentException { + return rowObject.getItemType(); + } + } + + private class ActionColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getColumnName() { + return "Action"; + } + + @Override + public int getColumnPreferredWidth() { + return 75; + } + + @Override + public String getValue(QuickFix rowObject, Settings settings, + ServiceProvider sp) throws IllegalArgumentException { + return rowObject.getActionName(); + } + } + + private class CurrentValueColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getColumnName() { + return "Current"; + } + + @Override + public int getColumnPreferredWidth() { + return 150; + } + + @Override + public String getValue(QuickFix rowObject, Settings settings, + ServiceProvider sp) throws IllegalArgumentException { + return rowObject.getCurrent(); + } + + @Override + public GColumnRenderer getColumnRenderer() { + return quickFixRenderer; + } + + } + + private class OriginalValueColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getColumnName() { + return "Original"; + } + + @Override + public int getColumnPreferredWidth() { + return 150; + } + + @Override + public String getValue(QuickFix rowObject, Settings settings, + ServiceProvider sp) throws IllegalArgumentException { + return rowObject.getOriginal(); + } + + @Override + public GColumnRenderer getColumnRenderer() { + return quickFixRenderer; + } + + } + + private class PreviewColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getColumnName() { + return "Preview"; + } + + @Override + public int getColumnPreferredWidth() { + return 150; + } + + @Override + public String getValue(QuickFix rowObject, Settings settings, + ServiceProvider sp) throws IllegalArgumentException { + return rowObject.getPreview(); + } + + @Override + public GColumnRenderer getColumnRenderer() { + return quickFixRenderer; + } + } + + private class AddressColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getColumnName() { + return "Address"; + } + + @Override + public int getColumnPreferredWidth() { + return 50; + } + + @Override + public Address getValue(QuickFix rowObject, Settings settings, + ServiceProvider sp) throws IllegalArgumentException { + return rowObject.getAddress(); + } + } + + private class PathColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getColumnName() { + return "Path"; + } + + @Override + public int getColumnPreferredWidth() { + return 150; + } + + @Override + public String getValue(QuickFix rowObject, Settings settings, + ServiceProvider sp) throws IllegalArgumentException { + return rowObject.getPath(); + } + } + + public class QuickFixRenderer extends AbstractGhidraColumnRenderer { + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + + JLabel renderer = (JLabel) super.getTableCellRendererComponent(data); + + QuickFix item = (QuickFix) data.getRowObject(); + StringBuilder buf = new StringBuilder(); + buf.append(""); + buf.append(""); + addHtmlTableRow(buf, "Action", item.getActionName() + " " + item.getItemType()); + + Address address = item.getAddress(); + if (address != null && address.isMemoryAddress()) { + addHtmlTableRow(buf, "Address", address.toString()); + } + addCustomTableRows(buf, item.getCustomToolTipData()); + + addHtmlTableRow(buf, "Original", item.getOriginal()); + addHtmlTableRow(buf, "Preview", item.getPreview()); + addHtmlTableRow(buf, "Current", item.getCurrent()); + buf.append("
    "); + + renderer.setToolTipText(buf.toString()); + + return renderer; + } + + private void addCustomTableRows(StringBuilder buf, Map dataMap) { + if (dataMap == null) { + return; + } + for (Entry entry : dataMap.entrySet()) { + addHtmlTableRow(buf, entry.getKey(), entry.getValue()); + } + } + + private void addHtmlTableRow(StringBuilder buf, String name, String value) { + buf.append(""); + buf.append(HTMLUtilities.escapeHTML(name)); + buf.append(":"); + buf.append(HTMLUtilities.escapeHTML(value)); + buf.append(""); + } + + @Override + public String getFilterString(String t, Settings settings) { + return t; + } + + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/TableDataLoader.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/TableDataLoader.java new file mode 100644 index 0000000000..03c9510c14 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/TableDataLoader.java @@ -0,0 +1,58 @@ +/* ### + * 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.quickfix; + +import docking.widgets.table.threaded.ThreadedTableModel; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * Generates table data for a {@link ThreadedTableModel}. Subclasses + * of ThreadedTableModel can call a TableLoader to supply data in the model's doLoad() method. Also + * has methods for the client to get feedback on the success of the load. + *

    + * The idea is that instead of having to subclass the table model to overload the doLoad() method, + * a general table model is sufficient and be handed a TableDataLoader to provide data to the model. + * + * @param The type of objects to load into a table model. + */ +public interface TableDataLoader { + + /** + * Loads data into the given accumulator + * @param accumulator the the accumulator for storing table data + * @param monitor the {@link TaskMonitor} + * @throws CancelledException if the operation is cancelled + */ + public void loadData(Accumulator accumulator, TaskMonitor monitor) + throws CancelledException; + + /** + * Returns true if at least one item was added to the accumulator. + * @return true if at least one item was added to the accumulator + */ + public boolean didProduceData(); + + /** + * Returns true if the load was terminated because the maximum number of items was + * reached. + * @return true if the load was terminated because the maximum number of items was + * reached. + */ + public boolean maxDataSizeReached(); + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/RenameQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/RenameQuickFix.java new file mode 100644 index 0000000000..04c71da3ca --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/RenameQuickFix.java @@ -0,0 +1,49 @@ +/* ### + * 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.replace; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.program.model.listing.Program; + +/** + * Base class for QuickFix objects that rename Ghidra program elements. + */ +public abstract class RenameQuickFix extends QuickFix { + + /** + * Constructor + * @param program the program this applies to + * @param name the original name of the element to rename + * @param newName the new name for the element when this QuickFix is applied. + */ + public RenameQuickFix(Program program, String name, String newName) { + super(program, name, newName); + validateReplacementName(); + } + + protected void validateReplacementName() { + if (replacement.isBlank()) { + setStatus(QuickFixStatus.ERROR, "Can't rename to \"\""); + } + } + + @Override + public String getActionName() { + return "Rename"; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceDialog.java new file mode 100644 index 0000000000..9579a7bf06 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceDialog.java @@ -0,0 +1,356 @@ +/* ### + * 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.replace; + +import java.awt.*; +import java.util.*; +import java.util.List; +import java.util.regex.PatternSyntaxException; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import docking.DialogComponentProvider; +import docking.widgets.combobox.GhidraComboBox; +import ghidra.app.util.HelpTopics; +import ghidra.framework.plugintool.PluginTool; +import ghidra.util.HelpLocation; +import ghidra.util.layout.*; + +/** + * Dialog for entering information to perform a search and replace operation. + */ +public class SearchAndReplaceDialog extends DialogComponentProvider { + + private static final int MAX_HISTORY = 20; + private List allTypes; + private List typeCheckboxes = new ArrayList<>(); + private SearchAndReplaceQuery query; + private Set selectedTypes = new HashSet<>(); + private GhidraComboBox searchTextComboBox; + private GhidraComboBox replaceTextComboBox; + private JCheckBox regexCheckbox; + private JCheckBox caseSensitiveCheckbox; + private JCheckBox wholeWordCheckbox; + private int searchLimit; + + /** + * Constructor + * @param searchLimit the maximum number of search matches to find before stopping. + */ + public SearchAndReplaceDialog(int searchLimit) { + super("Search And Replace"); + this.searchLimit = searchLimit; + this.allTypes = new ArrayList<>(SearchType.getSearchTypes()); + Collections.sort(allTypes); + addWorkPanel(buildMainPanel()); + addOKButton(); + addCancelButton(); + updateDialogStatus(); + setHelpLocation(new HelpLocation(HelpTopics.SEARCH, "Search And Replace")); + } + + /** + * Sets a new maximum number of search matches to find before stopping. + * @param searchLimit the new maximum number of search matches to find before stopping. + */ + public void setSearchLimit(int searchLimit) { + this.searchLimit = searchLimit; + } + + /** + * Convenience method for initializing the dialog, showing it and returning the query. + * @param tool the tool this dialog belongs to + * @return the SearchAndReplaceQuery generated by the information in the dialog when the + * "Ok" button is pressed, or null if the dialog was cancelled. + */ + public SearchAndReplaceQuery show(PluginTool tool) { + query = null; + searchTextComboBox.getTextField().selectAll(); + replaceTextComboBox.setText(""); + updateDialogStatus(); + tool.showDialog(this); + return query; + } + + /** + * Returns the query generated by the dialog when the "Ok" button is pressed or null if the + * dialog was cancelled. + * @return the SearchAndReplaceQuery generated by the information in the dialog when the + * "Ok" button is pressed, or null if the dialog was cancelled. + */ + public SearchAndReplaceQuery getQuery() { + return query; + } + + private JComponent buildMainPanel() { + JPanel panel = new JPanel(new VerticalLayout(20)); + panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + panel.add(buildPatternsPanel()); + panel.add(buildOptionsPanel()); + panel.add(buildTypesPanel()); + return panel; + } + + private JComponent buildTypesPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createTitledBorder("Search For:")); + panel.add(buildInnerTypesPanel(), BorderLayout.CENTER); + panel.add(buildSelectAllPanel(), BorderLayout.SOUTH); + return panel; + } + + private JPanel buildInnerTypesPanel() { + JPanel panel = new JPanel(new GridLayout(0, 3, 10, 5)); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + for (SearchType type : allTypes) { + JCheckBox cb = new JCheckBox(type.getName()); + typeCheckboxes.add(cb); + cb.setToolTipText(type.getDescription()); + cb.addActionListener(e -> typeCheckBoxChanged(cb, type)); + panel.add(cb); + } + return panel; + } + + private Component buildSelectAllPanel() { + JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 30, 10)); + JButton selectAllButton = new JButton("Select All"); + JButton deselectAllButton = new JButton("Deselect All"); + selectAllButton.addActionListener(e -> selectAllTypes()); + deselectAllButton.addActionListener(e -> deselectAllTypes()); + panel.add(selectAllButton); + panel.add(deselectAllButton); + return panel; + } + + private void selectAllTypes() { + for (JCheckBox checkBox : typeCheckboxes) { + checkBox.setSelected(true); + } + selectedTypes.addAll(allTypes); + updateDialogStatus(); + } + + private void deselectAllTypes() { + for (JCheckBox checkBox : typeCheckboxes) { + checkBox.setSelected(false); + } + selectedTypes.clear(); + updateDialogStatus(); + } + + private void typeCheckBoxChanged(JCheckBox cb, SearchType type) { + if (cb.isSelected()) { + selectedTypes.add(type); + } + else { + selectedTypes.remove(type); + } + updateDialogStatus(); + } + + private void updateDialogStatus() { + boolean isValid = hasValidInformation(); + setOkEnabled(isValid); + } + + private boolean hasValidInformation() { + clearStatusText(); + String searchText = searchTextComboBox.getText().trim(); + if (searchText.isBlank()) { + setStatusText("Please enter search text"); + return false; + } + if (selectedTypes.isEmpty()) { + setStatusText("Please select at least one \"search for\" item to search!"); + return false; + } + return createQuery() != null; + } + + private JComponent buildOptionsPanel() { + regexCheckbox = new JCheckBox("Regular expression"); + caseSensitiveCheckbox = new JCheckBox("Case sensitive"); + wholeWordCheckbox = new JCheckBox("Whole word"); + + regexCheckbox.addActionListener(e -> regExCheckboxChanged()); + + regexCheckbox.setToolTipText("Interprets search and replace text as regular expressions"); + caseSensitiveCheckbox.setToolTipText("Determines if search text is case sensitive"); + wholeWordCheckbox.setToolTipText("Sets if the input pattern must match whole words. For" + + " names, this means the entire name. For comments, it means matching entire words."); + + JPanel panel = new JPanel(new HorizontalLayout(10)); + Border titleBorder = BorderFactory.createTitledBorder("Options:"); + Border innerBorder = BorderFactory.createEmptyBorder(5, 5, 5, 5); + panel.setBorder(BorderFactory.createCompoundBorder(titleBorder, innerBorder)); + panel.add(regexCheckbox); + panel.add(caseSensitiveCheckbox); + panel.add(wholeWordCheckbox); + return panel; + } + + private void regExCheckboxChanged() { + wholeWordCheckbox.setEnabled(!regexCheckbox.isSelected()); + updateDialogStatus(); + } + + private Component buildPatternsPanel() { + searchTextComboBox = new GhidraComboBox<>(); + searchTextComboBox.setEditable(true); + searchTextComboBox.setToolTipText("Enter the text to search for"); + searchTextComboBox.getTextField() + .getDocument() + .addDocumentListener(new DocumentListener() { + + @Override + public void removeUpdate(DocumentEvent e) { + updateDialogStatus(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + updateDialogStatus(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + updateDialogStatus(); + } + }); + + replaceTextComboBox = new GhidraComboBox<>(); + replaceTextComboBox.setEditable(true); + replaceTextComboBox.setToolTipText("Enter the text to replace matched search text"); + + searchTextComboBox.addActionListener(e -> pressOk()); + replaceTextComboBox.addActionListener(e -> pressOk()); + + JPanel panel = new JPanel(new PairLayout(10, 10)); + panel.add(new JLabel("Find:", SwingConstants.RIGHT)); + panel.add(searchTextComboBox); + panel.add(new JLabel("Replace with:", SwingConstants.RIGHT)); + panel.add(replaceTextComboBox); + + return panel; + } + + private void pressOk() { + if (isOKEnabled()) { + okCallback(); + } + } + + @Override + protected void okCallback() { + query = createQuery(); + updateHistory(searchTextComboBox); + updateHistory(replaceTextComboBox); + closeDialog(); + } + + private SearchAndReplaceQuery createQuery() { + String searchText = searchTextComboBox.getText().trim(); + String replacePattern = replaceTextComboBox.getText().trim(); + boolean isRegEx = regexCheckbox.isSelected(); + boolean isCaseSensitive = caseSensitiveCheckbox.isSelected(); + boolean isWholeWord = wholeWordCheckbox.isSelected(); + Set searchTypes = getSelectedSearchTypes(); + try { + return new SearchAndReplaceQuery(searchText, replacePattern, searchTypes, isRegEx, + isCaseSensitive, isWholeWord, searchLimit); + } + catch (PatternSyntaxException e) { + return null; + } + } + + private Set getSelectedSearchTypes() { + Set set = new HashSet<>(); + for (SearchType type : allTypes) { + if (selectedTypes.contains(type)) { + set.add(type); + } + } + return set; + } + + private void updateHistory(GhidraComboBox combo) { + String value = combo.getText(); + if (value.isBlank()) { + return; + } + DefaultComboBoxModel model = + (DefaultComboBoxModel) combo.getModel(); + model.removeElement(value); + if (model.getSize() > MAX_HISTORY) { + model.removeElementAt(model.getSize() - 1); + } + model.insertElementAt(value, 0); + model.setSelectedItem(value); + } + + /** + * Sets the search and replace text fields with given values. + * @param searchText the text to be put in the search field + * @param replaceText the text to be put in the replace field + */ + public void setSarchAndReplaceText(String searchText, String replaceText) { + searchTextComboBox.setText(searchText); + replaceTextComboBox.setText(replaceText); + } + + /** + * Sets the search type with the given name to be selected. + * @param searchType the name of the search type to select + */ + public void selectSearchType(String searchType) { + for (JCheckBox checkBox : typeCheckboxes) { + if (checkBox.getText().equals(searchType)) { + checkBox.setSelected(true); + break; + } + } + for (SearchType type : allTypes) { + if (type.getName().equals(searchType)) { + selectedTypes.add(type); + break; + } + } + updateDialogStatus(); + } + + /** + * Returns true if the "ok" button is enabled. + * @return true if the "ok" button is enabled. + */ + public boolean isOkEnabled() { + return super.isOKEnabled(); + } + + /** + * Selects the RegEx checkbox in the dialog. + * @param b true to select RegEx, false to turn deselect it + */ + public void selectRegEx(boolean b) { + regexCheckbox.setSelected(b); + updateDialogStatus(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceHandler.java new file mode 100644 index 0000000000..2c6241e2df --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceHandler.java @@ -0,0 +1,68 @@ +/* ### + * 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.replace; + +import java.util.HashSet; +import java.util.Set; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.program.model.listing.Program; +import ghidra.util.classfinder.ExtensionPoint; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * Base class for discoverable SearchAndReplaceHandlers. A SearchAndReplaceHandler is responsible + * for searching one or more specific program elements (referred to as {@link SearchType}) for a + * given search pattern and generating the appropriate {@link QuickFix}. + *

    + * Typically, one handler will handle related search elements for efficiency. For example, the + * DataTypesSearchAndReplaceHandler is responsible for datatype names, field names, field comments, + * etc. The idea is to only loop through all the datatypes once, regardless of what aspect of a + * datatype you are searching for. + */ +public abstract class SearchAndReplaceHandler implements ExtensionPoint { + private Set types = new HashSet<>(); + + /** + * Method to perform the search for the pattern and options as specified by the given + * SearchAndReplaceQuery. As matches are found, appropriate {@link QuickFix}s are added to + * the given accumulator. + * @param program the program being searched + * @param query contains the search pattern, replacement pattern, and options related to the + * query. + * @param accumulator the accumulator that resulting QuickFix items are added to as they are + * found. + * @param monitor a {@link TaskMonitor} for reporting progress and checking if the search has + * been cancelled. + * @throws CancelledException thrown if the operation has been cancelled via the taskmonitor + */ + public abstract void findAll(Program program, SearchAndReplaceQuery query, + Accumulator accumulator, TaskMonitor monitor) throws CancelledException; + + /** + * Returns the set of {@link SearchType}s this handler supports. + * @return the set of {@link SearchType}s this handler supports. + */ + public Set getSearchAndReplaceTypes() { + return types; + } + + protected void addType(SearchType type) { + types.add(type); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplacePlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplacePlugin.java new file mode 100644 index 0000000000..2f57d2e0ea --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplacePlugin.java @@ -0,0 +1,133 @@ +/* ### + * 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.replace; + +import static ghidra.app.util.SearchConstants.*; + +import java.util.ArrayList; +import java.util.List; + +import docking.action.builder.ActionBuilder; +import ghidra.app.CorePluginPackage; +import ghidra.app.context.NavigatableActionContext; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.ProgramPlugin; +import ghidra.app.services.GoToService; +import ghidra.app.services.ProgramManager; +import ghidra.app.util.HelpTopics; +import ghidra.app.util.SearchConstants; +import ghidra.framework.options.OptionsChangeListener; +import ghidra.framework.options.ToolOptions; +import ghidra.framework.plugintool.PluginInfo; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.program.model.listing.Program; +import ghidra.util.HelpLocation; +import ghidra.util.bean.opteditor.OptionsVetoException; + +/** + * Plugin to perform search and replace operations for many different program element types such + * as labels, functions, classes, datatypes, memory blocks, and more. + */ +//@formatter:off +@PluginInfo( + status = PluginStatus.RELEASED, + packageName = CorePluginPackage.NAME, + category = PluginCategoryNames.SEARCH, + shortDescription = "Search and replace text on program element names or comments.", + description = "This plugin provides a search and replace capability for a variety of" + + "program elements including functions, classes, namespaces, datatypes, field names, and" + + "other.", + servicesRequired = { ProgramManager.class, GoToService.class } +) +//@formatter:on +public class SearchAndReplacePlugin extends ProgramPlugin { + + private SearchAndReplaceDialog cachedDialog; + private int searchLimit = DEFAULT_SEARCH_LIMIT; + private OptionsChangeListener searchOptionsListener; + private List providers = new ArrayList<>(); + + public SearchAndReplacePlugin(PluginTool plugintool) { + super(plugintool); + createActions(); + initializeOptions(); + } + + @Override + protected void programClosed(Program program) { + List copy = new ArrayList<>(providers); + for (SearchAndReplaceProvider provider : copy) { + provider.programClosed(program); + } + } + + private void initializeOptions() { + ToolOptions options = tool.getOptions(SearchConstants.SEARCH_OPTION_NAME); + searchLimit = options.getInt(SEARCH_LIMIT_NAME, DEFAULT_SEARCH_LIMIT); + + searchOptionsListener = this::searchOptionsChanged; + options.addOptionsChangeListener(searchOptionsListener); + } + + private void searchOptionsChanged(ToolOptions options, String optionName, Object oldValue, + Object newValue) { + + if (optionName.equals(SEARCH_LIMIT_NAME)) { + int limit = (int) newValue; + if (limit <= 0) { + throw new OptionsVetoException("Search limit must be greater than 0"); + } + searchLimit = limit; + if (cachedDialog != null) { + cachedDialog.setSearchLimit(limit); + } + } + } + + private void createActions() { + new ActionBuilder("Search And Replace", getName()) + .menuPath("&Search", "Search And Replace...") + .menuGroup("search", "d") + .description("Search and replace names of various program elements") + .helpLocation(new HelpLocation(HelpTopics.SEARCH, "Search And Replace")) + .withContext(NavigatableActionContext.class, true) + .onAction(this::searchAndReplace) + .buildAndInstall(tool); + } + + private void searchAndReplace(NavigatableActionContext c) { + SearchAndReplaceDialog dialog = getDialog(); + SearchAndReplaceQuery query = dialog.show(tool); + if (query == null) { + return; + } + + Program program = c.getProgram(); + providers.add(new SearchAndReplaceProvider(this, program, query)); + } + + private SearchAndReplaceDialog getDialog() { + if (cachedDialog == null) { + cachedDialog = new SearchAndReplaceDialog(searchLimit); + } + return cachedDialog; + } + + void providerClosed(SearchAndReplaceProvider provider) { + providers.remove(provider); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceProvider.java new file mode 100644 index 0000000000..c61d984ce9 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceProvider.java @@ -0,0 +1,106 @@ +/* ### + * 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.replace; + +import java.awt.*; + +import javax.swing.*; + +import ghidra.app.util.HelpTopics; +import ghidra.features.base.quickfix.*; +import ghidra.program.model.listing.Program; +import ghidra.util.HelpLocation; +import ghidra.util.Msg; + +/** + * Subclass of the {@link QuckFixTableProvider} that customizes it specifically for search and replace + * operations. + */ +public class SearchAndReplaceProvider extends QuckFixTableProvider { + + private SearchAndReplacePlugin plugin; + private SearchAndReplaceQuery query; + + public SearchAndReplaceProvider(SearchAndReplacePlugin plugin, Program program, + SearchAndReplaceQuery query) { + + super(plugin.getTool(), "Search And Replace", plugin.getName(), program, + new SearchAndReplaceQuckFixTableLoader(program, query)); + this.plugin = plugin; + this.query = query; + setTitle(generateTitle()); + setTabText(getTabTitle()); + addToTool(); + setHelpLocation(new HelpLocation(HelpTopics.SEARCH, "Search_And_Replace_Results")); + } + + @Override + protected void tableLoaded(boolean wasCancelled, TableDataLoader loader) { + if (!loader.didProduceData()) { + Msg.showInfo(getClass(), getComponent(), "No Results Found!", + "No results for \"" + query.getSearchText() + "\" found."); + closeComponent(); + return; + } + setVisible(true); + if (loader.maxDataSizeReached()) { + Msg.showInfo(getClass(), getComponent(), "Search Limit Exceeded!", + "Stopped search after finding " + query.getSearchLimit() + " matches.\n" + + "The search limit can be changed at Edit->Tool Options, under Search."); + } + toFront(); + } + + @Override + public void closeComponent() { + super.closeComponent(); + plugin.providerClosed(this); + } + + private String getTabTitle() { + return "\"" + query.getSearchText() + "\" -> \"" + + query.getReplacementText() + "\""; + } + + private String generateTitle() { + return "Search & Replace: " + getTabTitle(); + } + + @Override + protected JPanel buildMainPanel() { + JPanel quickFixPanel = super.buildMainPanel(); + + JPanel panel = new JPanel(new BorderLayout()); + panel.add(quickFixPanel, BorderLayout.CENTER); + panel.add(buildButtonPanel(), BorderLayout.SOUTH); + + return panel; + } + + private Component buildButtonPanel() { + JButton replaceButton = new JButton("Replace All"); + JButton dismissButton = new JButton("Dismiss"); + replaceButton.addActionListener(e -> executeAll()); + dismissButton.addActionListener(e -> closeComponent()); + + JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 0)); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + panel.add(replaceButton); + panel.add(dismissButton); + return panel; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceQuckFixTableLoader.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceQuckFixTableLoader.java new file mode 100644 index 0000000000..67899f852c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceQuckFixTableLoader.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.replace; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.quickfix.TableDataLoader; +import ghidra.program.model.listing.Program; +import ghidra.util.datastruct.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * Class for loading search and replace items into a ThreadedTableModel. + */ +public class SearchAndReplaceQuckFixTableLoader implements TableDataLoader { + + private SearchAndReplaceQuery query; + private Program program; + private boolean hasResults; + private boolean searchLimitExceeded; + + public SearchAndReplaceQuckFixTableLoader(Program program, SearchAndReplaceQuery query) { + this.program = program; + this.query = query; + } + + @Override + public void loadData(Accumulator accumulator, TaskMonitor monitor) + throws CancelledException { + + Accumulator wrappedAccumulator = + new SizeRestrictedAccumulatorWrapper(accumulator, query.getSearchLimit()); + try { + query.findAll(program, wrappedAccumulator, monitor); + } + catch (AccumulatorSizeException e) { + searchLimitExceeded = true; + } + finally { + hasResults = !accumulator.isEmpty(); + } + } + + @Override + public boolean didProduceData() { + return hasResults; + } + + @Override + public boolean maxDataSizeReached() { + return searchLimitExceeded; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceQuery.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceQuery.java new file mode 100644 index 0000000000..94ae1bc7b8 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchAndReplaceQuery.java @@ -0,0 +1,152 @@ +/* ### + * 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.replace; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.program.model.listing.Program; +import ghidra.util.UserSearchUtils; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * Immutable class for storing all related query information for performing a search and + * replace operation. It includes the search pattern, the search pattern text, the search lmiit, + * and the types of program elements to search. + */ +public class SearchAndReplaceQuery { + private final String searchText; + private final String replacementText; + private final Pattern pattern; + private final int searchLimit; + private final Set selectedTypes = new HashSet<>(); + + /** + * Constructor + * @param searchText the user entered search pattern text. It will be used to generate the + * actual Pattern based on the various options. + * @param replacementText the user entered replacement text. + * @param searchTypes the types of program elements to search + * @param isRegEx true if the given search text is to be interpreted as a regular expression. + * @param isCaseSensitive true if the search text should be case sensitive + * @param isWholeWord true, the search text should match the enter element in the case of a + * rename, or an entire word within a larger sentence in the case of a comment. + * @param searchLimit the maximum entries to find before terminating the search. + */ + public SearchAndReplaceQuery(String searchText, String replacementText, + Set searchTypes, boolean isRegEx, boolean isCaseSensitive, + boolean isWholeWord, int searchLimit) { + this.searchText = searchText; + this.replacementText = replacementText; + this.pattern = createPattern(isRegEx, isCaseSensitive, isWholeWord); + this.searchLimit = searchLimit; + selectedTypes.addAll(searchTypes); + } + + /** + * Method to initiate the search. + * @param program the program to search + * @param accumulator the accumulator to store the generated {@link QuickFix}s + * @param monitor the {@link TaskMonitor} + * @throws CancelledException if the search is cancelled. + */ + public void findAll(Program program, Accumulator accumulator, + TaskMonitor monitor) throws CancelledException { + Set handlers = getHandlers(); + for (SearchAndReplaceHandler handler : handlers) { + handler.findAll(program, this, accumulator, monitor); + } + } + + /** + * Returns the search {@link Pattern} used to search program elements. + * @return the search {@link Pattern} used to search program elements + */ + public Pattern getSearchPattern() { + return pattern; + } + + /** + * Returns true if the given SearchType is to be included in the search. + * @param searchType the SearchType to check if it is included in the search + * @return true if the given SearchType is to be included in the search. + */ + public boolean containsSearchType(SearchType searchType) { + return selectedTypes.contains(searchType); + } + + /** + * Returns the search text used to generate the pattern for this query. + * @return the search text used to generate the pattern for this query + */ + public String getSearchText() { + return searchText; + } + + /** + * Returns the replacement text that will replace matched elements. + * @return the replacement text that will replace matched elements + */ + public String getReplacementText() { + return replacementText; + } + + /** + * Returns a set of all the SearchTypes to be included in this query. + * @return a set of all the SearchTypes to be included in this query + */ + public Set getSelectedSearchTypes() { + return selectedTypes; + } + + /** + * Returns the maximum number of search matches to be found before stopping early. + * @return the maximum number of search matches to be found before stopping early. + */ + public int getSearchLimit() { + return searchLimit; + } + + private Pattern createPattern(boolean isRegEx, boolean isCaseSensitive, boolean isWholeWord) { + int regExFlags = Pattern.DOTALL; + if (!isCaseSensitive) { + regExFlags |= Pattern.CASE_INSENSITIVE; + } + + if (isRegEx) { + return Pattern.compile(searchText, regExFlags); + } + + String converted = UserSearchUtils.convertUserInputToRegex(searchText, false); + if (isWholeWord) { + converted = "\\b" + converted + "\\b"; + } + + return Pattern.compile(converted, regExFlags); + } + + private Set getHandlers() { + Set handlers = new HashSet<>(); + for (SearchType type : selectedTypes) { + handlers.add(type.getHandler()); + } + return handlers; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchType.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchType.java new file mode 100644 index 0000000000..b8e668cfc3 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/SearchType.java @@ -0,0 +1,93 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.replace; + +import java.util.*; + +import ghidra.util.classfinder.ClassSearcher; + +/** + * Represents a ghidra program element type that can be individually included or excluded when doing + * a search and replace operation. The {@link SearchAndReplaceDialog} will include a checkbox for + * each of these types. + */ +public class SearchType implements Comparable { + private final SearchAndReplaceHandler handler; + private final String name; + private final String description; + + /** + * Constructor + * @param handler The {@link SearchAndReplaceHandler} that actually has the logic for doing + * the search for this program element type. + * @param name the name of element type that is searchable + * @param description a description of this type which would be suitable to display as a tooltip + */ + public SearchType(SearchAndReplaceHandler handler, String name, String description) { + this.handler = handler; + this.name = name; + this.description = description; + } + + /** + * Returns the {@link SearchAndReplaceHandler} that can process this type. + * @return the handler for processing this type + */ + public SearchAndReplaceHandler getHandler() { + return handler; + } + + /** + * Returns the name of this search type. + * @return the name of this search type + */ + public String getName() { + return name; + } + + /** + * Returns a description of this search type. + * @return a description of this search type + */ + public String getDescription() { + return description; + } + + /** + * Static convenience method for finding all known SearchTypes. It uses the + * {@link ClassSearcher} to find all {@link SearchAndReplaceHandler}s and then gathers up + * all the SearchTypes that each handler supports. + * + * @return The set of all Known SearchTypes + */ + public static Set getSearchTypes() { + List handlers = + ClassSearcher.getInstances(SearchAndReplaceHandler.class); + + Set types = new HashSet<>(); + + for (SearchAndReplaceHandler handler : handlers) { + types.addAll(handler.getSearchAndReplaceTypes()); + } + + return types; + } + + @Override + public int compareTo(SearchType o) { + return name.compareTo(o.name); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/DataTypesSearchAndReplaceHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/DataTypesSearchAndReplaceHandler.java new file mode 100644 index 0000000000..ae0d373ea7 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/DataTypesSearchAndReplaceHandler.java @@ -0,0 +1,274 @@ +/* ### + * 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.replace.handler; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.replace.*; +import ghidra.features.base.replace.items.*; +import ghidra.program.model.data.*; +import ghidra.program.model.data.Enum; +import ghidra.program.model.listing.Program; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * {@link SearchAndReplaceHandler} for handling search and replace for datatype names, + * structure and union field names, structure and union field comments, enum value names, + * and enum value comments. + */ +public class DataTypesSearchAndReplaceHandler extends SearchAndReplaceHandler { + DataTypeSearchType nameType; + DataTypeSearchType datatypeCommentsType; + DataTypeSearchType fieldNameType; + DataTypeSearchType enumValueNameType; + + public DataTypesSearchAndReplaceHandler() { + nameType = new NameSearchType(this); + datatypeCommentsType = new DataTypeCommentsSearchType(this); + fieldNameType = new FieldNameSearchType(this); + enumValueNameType = new EnumValueSearchType(this); + + addType(nameType); + addType(datatypeCommentsType); + addType(fieldNameType); + addType(enumValueNameType); + } + + @Override + public void findAll(Program program, SearchAndReplaceQuery query, + Accumulator accumulator, TaskMonitor monitor) throws CancelledException { + + ProgramBasedDataTypeManager dataTypeManager = program.getDataTypeManager(); + List allDataTypes = new ArrayList<>(); + dataTypeManager.getAllDataTypes(allDataTypes); + + monitor.initialize(allDataTypes.size(), "Searching DataTypes..."); + + boolean doNames = query.containsSearchType(nameType); + boolean doDatatypeComments = query.containsSearchType(datatypeCommentsType); + boolean doFieldNames = query.containsSearchType(fieldNameType); + boolean doEnumValueNames = query.containsSearchType(enumValueNameType); + + for (DataType dataType : allDataTypes) { + monitor.increment(); + if (dataType instanceof Pointer || dataType instanceof Array) { + continue; + } + if (doNames) { + nameType.search(program, dataType, query, accumulator); + } + if (doDatatypeComments) { + datatypeCommentsType.search(program, dataType, query, accumulator); + } + if (doFieldNames) { + fieldNameType.search(program, dataType, query, accumulator); + } + if (doEnumValueNames) { + enumValueNameType.search(program, dataType, query, accumulator); + } + } + } + + private abstract static class DataTypeSearchType extends SearchType { + public DataTypeSearchType(SearchAndReplaceHandler handler, String name, + String description) { + super(handler, name, description); + } + + protected abstract void search(Program program, DataType dataType, + SearchAndReplaceQuery query, Accumulator accumulator); + + } + + private static class NameSearchType extends DataTypeSearchType { + public NameSearchType(SearchAndReplaceHandler handler) { + super(handler, "Datatypes", "Search and replace datatype names"); + } + + @Override + protected void search(Program program, DataType dataType, SearchAndReplaceQuery query, + Accumulator accumulator) { + + Pattern searchPattern = query.getSearchPattern(); + Matcher matcher = searchPattern.matcher(dataType.getName()); + if (matcher.find()) { + String newName = matcher.replaceAll(query.getReplacementText()); + RenameDataTypeQuickFix item = + new RenameDataTypeQuickFix(program, dataType, newName); + accumulator.add(item); + } + } + + } + + private static class FieldNameSearchType extends DataTypeSearchType { + public FieldNameSearchType(SearchAndReplaceHandler handler) { + super(handler, "Datatype Fields", + "Search and replace structure and union member names"); + } + + @Override + protected void search(Program program, DataType dataType, SearchAndReplaceQuery query, + Accumulator accumulator) { + + if (!(dataType instanceof Composite composite)) { + return; + } + DataTypeComponent[] definedComponents = composite.getDefinedComponents(); + Pattern searchPattern = query.getSearchPattern(); + + for (int i = 0; i < definedComponents.length; i++) { + DataTypeComponent component = definedComponents[i]; + String name = getFieldName(component); + Matcher matcher = searchPattern.matcher(name); + if (matcher.find()) { + String newName = matcher.replaceAll(query.getReplacementText()); + int ordinal = component.getOrdinal(); + QuickFix item = + new RenameFieldQuickFix(program, composite, ordinal, name, newName); + accumulator.add(item); + } + } + } + + private String getFieldName(DataTypeComponent component) { + String fieldName = component.getFieldName(); + return fieldName == null ? component.getDefaultFieldName() : fieldName; + } + } + + private static class DataTypeCommentsSearchType extends DataTypeSearchType { + public DataTypeCommentsSearchType(SearchAndReplaceHandler handler) { + super(handler, "Datatype Comments", "Search and replace comments on datatypes"); + } + + @Override + protected void search(Program program, DataType dataType, SearchAndReplaceQuery query, + Accumulator accumulator) { + searchDescriptions(program, dataType, query, accumulator); + + if (dataType instanceof Composite composite) { + searchFieldComments(program, composite, query, accumulator); + } + else if (dataType instanceof Enum enumm) { + searchEnumComments(program, enumm, query, accumulator); + } + } + + private void searchEnumComments(Program program, Enum enumm, SearchAndReplaceQuery query, + Accumulator accumulator) { + String[] names = enumm.getNames(); + Pattern searchPattern = query.getSearchPattern(); + for (int i = 0; i < names.length; i++) { + String valueName = names[i]; + String comment = enumm.getComment(valueName); + Matcher matcher = searchPattern.matcher(comment); + if (matcher.find()) { + String newValueName = matcher.replaceAll(query.getReplacementText()); + QuickFix item = + new UpdateEnumCommentQuickFix(program, enumm, valueName, newValueName); + accumulator.add(item); + } + } + } + + private void searchFieldComments(Program program, Composite composite, + SearchAndReplaceQuery query, Accumulator accumulator) { + + DataTypeComponent[] definedComponents = composite.getDefinedComponents(); + Pattern searchPattern = query.getSearchPattern(); + + for (int i = 0; i < definedComponents.length; i++) { + DataTypeComponent component = definedComponents[i]; + String comment = component.getComment(); + if (comment == null) { + continue; + } + Matcher matcher = searchPattern.matcher(comment); + if (matcher.find()) { + String newComment = matcher.replaceAll(query.getReplacementText()); + QuickFix item = + new UpdateFieldCommentQuickFix(program, composite, component.getFieldName(), + component.getOrdinal(), comment, newComment); + accumulator.add(item); + } + } + } + + protected void searchDescriptions(Program program, DataType dataType, + SearchAndReplaceQuery query, Accumulator accumulator) { + + String description = getDescription(dataType); + if (description == null || description.isBlank()) { + return; + } + Pattern searchPattern = query.getSearchPattern(); + Matcher matcher = searchPattern.matcher(description); + if (matcher.find()) { + String newName = matcher.replaceAll(query.getReplacementText()); + UpdateDataTypeDescriptionQuickFix item = + new UpdateDataTypeDescriptionQuickFix(program, dataType, newName); + accumulator.add(item); + } + } + + private String getDescription(DataType dataType) { + if (dataType instanceof Composite composite) { + return composite.getDescription(); + } + if (dataType instanceof Enum enumDataType) { + return enumDataType.getDescription(); + } + return null; + } + + } + + private static class EnumValueSearchType extends DataTypeSearchType { + public EnumValueSearchType(SearchAndReplaceHandler handler) { + super(handler, "Enum Values", "Search and replace enum value names"); + } + + @Override + protected void search(Program program, DataType dataType, SearchAndReplaceQuery query, + Accumulator accumulator) { + + if (!(dataType instanceof Enum enumm)) { + return; + } + String[] names = enumm.getNames(); + Pattern searchPattern = query.getSearchPattern(); + + for (int i = 0; i < names.length; i++) { + String valueName = names[i]; + Matcher matcher = searchPattern.matcher(valueName); + if (matcher.find()) { + String newValueName = matcher.replaceAll(query.getReplacementText()); + QuickFix item = + new RenameEnumValueQuickFix(program, enumm, valueName, newValueName); + accumulator.add(item); + } + } + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/DatatypeCategorySearchAndReplaceHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/DatatypeCategorySearchAndReplaceHandler.java new file mode 100644 index 0000000000..f97e2a6cb4 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/DatatypeCategorySearchAndReplaceHandler.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.replace.handler; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.replace.*; +import ghidra.features.base.replace.items.RenameCategoryQuickFix; +import ghidra.program.model.data.Category; +import ghidra.program.model.data.DataTypeManager; +import ghidra.program.model.listing.Program; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utility.function.ExceptionalConsumer; + +/** + * {@link SearchAndReplaceHandler} for handling search and replace for datatype category names. + */ +public class DatatypeCategorySearchAndReplaceHandler extends SearchAndReplaceHandler { + + public DatatypeCategorySearchAndReplaceHandler() { + addType(new SearchType(this, "Datatype Categories", + "Search and replace datatype category names")); + } + + @Override + public void findAll(Program program, SearchAndReplaceQuery query, + Accumulator accumulator, TaskMonitor monitor) throws CancelledException { + + int categoryCount = program.getDataTypeManager().getCategoryCount(); + monitor.initialize(categoryCount, "Searching Datatype categories..."); + + Pattern pattern = query.getSearchPattern(); + + DataTypeManager dtm = program.getDataTypeManager(); + Category rootCategory = dtm.getRootCategory(); + + visitRecursively(rootCategory, category -> { + monitor.increment(); + Matcher matcher = pattern.matcher(category.getName()); + if (matcher.find()) { + String newName = matcher.replaceAll(query.getReplacementText()); + RenameCategoryQuickFix item = new RenameCategoryQuickFix(program, category, newName); + accumulator.add(item); + if (accumulator.size() >= query.getSearchLimit()) { + return; + } + } + }); + + } + + private void visitRecursively(Category category, + ExceptionalConsumer callback) throws CancelledException { + + callback.accept(category); + Category[] categories = category.getCategories(); + for (Category childCategory : categories) { + visitRecursively(childCategory, callback); + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/ListingCommentsSearchAndReplaceHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/ListingCommentsSearchAndReplaceHandler.java new file mode 100644 index 0000000000..d4c436c745 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/ListingCommentsSearchAndReplaceHandler.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.replace.handler; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.replace.*; +import ghidra.features.base.replace.items.UpdateCommentQuickFix; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.*; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * {@link SearchAndReplaceHandler} for handling search and replace for listing comments on + * instructions or data. + */ +public class ListingCommentsSearchAndReplaceHandler extends SearchAndReplaceHandler { + + public ListingCommentsSearchAndReplaceHandler() { + addType(new SearchType(this, "Comments", "Search and replace in listing comments")); + } + + @Override + public void findAll(Program program, SearchAndReplaceQuery query, + Accumulator accumulator, TaskMonitor monitor) throws CancelledException { + + Listing listing = program.getListing(); + long count = listing.getCommentAddressCount(); + monitor.initialize(count, "Searching Comments..."); + + Pattern pattern = query.getSearchPattern(); + String replaceMentText = query.getReplacementText(); + + for (Address address : listing.getCommentAddressIterator(program.getMemory(), true)) { + monitor.checkCancelled(); + CodeUnitComments comments = listing.getAllComments(address); + for (CommentType type : CommentType.values()) { + String comment = comments.getComment(type); + String newComment = checkMatch(pattern, comment, replaceMentText); + if (newComment != null) { + accumulator.add( + new UpdateCommentQuickFix(program, address, type, comment, newComment)); + } + } + } + } + + private String checkMatch(Pattern pattern, String comment, String replacementText) { + if (comment == null) { + return null; + } + Matcher matcher = pattern.matcher(comment); + if (matcher.find()) { + return matcher.replaceAll(replacementText); + } + return null; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/MemoryBlockSearchAndReplaceHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/MemoryBlockSearchAndReplaceHandler.java new file mode 100644 index 0000000000..c7c3026cd8 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/MemoryBlockSearchAndReplaceHandler.java @@ -0,0 +1,64 @@ +/* ### + * 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.replace.handler; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.replace.*; +import ghidra.features.base.replace.items.RenameMemoryBlockQuickFix; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.Memory; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * {@link SearchAndReplaceHandler} for handling search and replace for memory block names. + */ + +public class MemoryBlockSearchAndReplaceHandler extends SearchAndReplaceHandler { + + public MemoryBlockSearchAndReplaceHandler() { + addType(new SearchType(this, "Memory Blocks", "Search and replace memory block names")); + } + + @Override + public void findAll(Program program, SearchAndReplaceQuery query, + Accumulator accumulator, TaskMonitor monitor) throws CancelledException { + + Memory memory = program.getMemory(); + MemoryBlock[] blocks = memory.getBlocks(); + monitor.initialize(blocks.length, "Searching MemoryBlocks..."); + + Pattern pattern = query.getSearchPattern(); + + for (MemoryBlock block : blocks) { + monitor.increment(); + Matcher matcher = pattern.matcher(block.getName()); + if (matcher.find()) { + String newName = matcher.replaceAll(query.getReplacementText()); + RenameMemoryBlockQuickFix item = + new RenameMemoryBlockQuickFix(program, block, newName); + accumulator.add(item); + } + + } + + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/ProgramTreeSearchAndReplaceHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/ProgramTreeSearchAndReplaceHandler.java new file mode 100644 index 0000000000..3b4dd38127 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/ProgramTreeSearchAndReplaceHandler.java @@ -0,0 +1,100 @@ +/* ### + * 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.replace.handler; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.replace.*; +import ghidra.features.base.replace.items.RenameProgramTreeGroupQuickFix; +import ghidra.program.model.listing.*; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * {@link SearchAndReplaceHandler} for handling search and replace for program tree modules and + * fragments. + */ + +public class ProgramTreeSearchAndReplaceHandler extends SearchAndReplaceHandler { + + public ProgramTreeSearchAndReplaceHandler() { + addType(new SearchType(this, "Program Trees", + "Search and replace program tree module and fragment names")); + } + + @Override + public void findAll(Program program, SearchAndReplaceQuery query, + Accumulator accumulator, TaskMonitor monitor) throws CancelledException { + + Listing listing = program.getListing(); + String[] treeNames = listing.getTreeNames(); + monitor.initialize(treeNames.length, "Search Program Trees"); + for (String treeName : treeNames) { + monitor.increment(); + findAll(program, treeName, query, accumulator, monitor); + } + } + + private void findAll(Program program, String treeName, SearchAndReplaceQuery query, + Accumulator accumulator, TaskMonitor monitor) throws CancelledException { + + Set set = gatherProgramTreeGroups(program, treeName, monitor); + Pattern pattern = query.getSearchPattern(); + + /** + * Check all the modules and fragments in the tree + */ + for (Group group : set) { + String name = group.getName(); + Matcher matcher = pattern.matcher(name); + if (matcher.find()) { + String newName = matcher.replaceAll(query.getReplacementText()); + QuickFix item = new RenameProgramTreeGroupQuickFix(program, group, newName); + accumulator.add(item); + } + } + + } + + private Set gatherProgramTreeGroups(Program program, String treeName, + TaskMonitor monitor) throws CancelledException { + monitor.checkCancelled(); + Listing listing = program.getListing(); + ProgramModule rootModule = listing.getRootModule(treeName); + + Set set = new HashSet<>(); + addProgramTreeGroupsRecursively(set, rootModule, monitor); + + // The root module name is the name of the program. Don't allow to change it here. + set.remove(rootModule); + return set; + } + + private void addProgramTreeGroupsRecursively(Set set, Group group, TaskMonitor monitor) { + set.add(group); + if (group instanceof ProgramModule module) { + Group[] children = module.getChildren(); + for (Group child : children) { + addProgramTreeGroupsRecursively(set, child, monitor); + } + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/SymbolsSearchAndReplaceHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/SymbolsSearchAndReplaceHandler.java new file mode 100644 index 0000000000..6df9546a44 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/handler/SymbolsSearchAndReplaceHandler.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.replace.handler; + +import static ghidra.program.model.symbol.SymbolType.*; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.replace.*; +import ghidra.features.base.replace.items.RenameSymbolQuickFix; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.*; +import ghidra.util.datastruct.Accumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * {@link SearchAndReplaceHandler} for handling search and replace for symbols. Specifically, it + * provides {@link SearchType}s for renaming labels, functions, namespaces, classes, local + * variables, and parameters. + */ +public class SymbolsSearchAndReplaceHandler extends SearchAndReplaceHandler { + + public SymbolsSearchAndReplaceHandler() { + //@formatter:off + addType(new SymbolSearchType(LABEL, "Labels", "Search and replace label names")); + addType(new SymbolSearchType(FUNCTION, "Functions", "Search and replace function names")); + addType(new SymbolSearchType(NAMESPACE, "Namespaces", "Search and replace generic namespace names")); + addType(new SymbolSearchType(CLASS, "Classes", "Search and replace class names")); + addType(new SymbolSearchType(LOCAL_VAR, "Local Variables", "Search and replace local variable names")); + addType(new SymbolSearchType(PARAMETER, "Parameters", "Search and replace parameter names")); + //@formatter:on + } + + @Override + public void findAll(Program program, SearchAndReplaceQuery query, + Accumulator accumulator, TaskMonitor monitor) throws CancelledException { + + SymbolTable symbolTable = program.getSymbolTable(); + int symbolCount = symbolTable.getNumSymbols(); + monitor.initialize(symbolCount, "Searching Labels..."); + + Pattern pattern = query.getSearchPattern(); + + Set selectedSymbolTypes = getSelectedSymbolTypes(query); + + for (Symbol symbol : symbolTable.getDefinedSymbols()) { + monitor.increment(); + if (symbol.isExternal()) { + continue; + } + + SymbolType symbolType = symbol.getSymbolType(); + + if (selectedSymbolTypes.contains(symbolType)) { + if (symbolType == SymbolType.FUNCTION) { + Function function = (Function) symbol.getObject(); + // Thunks can't be renamed directly + if (function.isThunk()) { + continue; + } + } + Matcher matcher = pattern.matcher(symbol.getName()); + if (matcher.find()) { + String newName = matcher.replaceAll(query.getReplacementText()); + RenameSymbolQuickFix item = new RenameSymbolQuickFix(symbol, newName); + accumulator.add(item); + } + } + } + } + + private Set getSelectedSymbolTypes(SearchAndReplaceQuery query) { + Set symbolTypes = new HashSet<>(); + + Set selectedSearchTypes = query.getSelectedSearchTypes(); + for (SearchType searchType : selectedSearchTypes) { + if (searchType instanceof SymbolSearchType symbolSearchType) { + symbolTypes.add(symbolSearchType.getSymbolType()); + } + } + return symbolTypes; + } + + private class SymbolSearchType extends SearchType { + private final SymbolType symbolType; + + SymbolSearchType(SymbolType symbolType, String name, String description) { + super(SymbolsSearchAndReplaceHandler.this, name, description); + this.symbolType = symbolType; + } + + SymbolType getSymbolType() { + return symbolType; + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/CompositeFieldQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/CompositeFieldQuickFix.java new file mode 100644 index 0000000000..c78ddd39e9 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/CompositeFieldQuickFix.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.replace.items; + +import java.util.Map; + +import ghidra.app.services.DataTypeManagerService; +import ghidra.features.base.quickfix.QuickFix; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.Composite; +import ghidra.program.model.data.DataTypeComponent; +import ghidra.program.model.listing.Program; + +/** + * Base class for Composite field Quick Fixes. Primarily exists to host the logic for finding + * components in a composite even as it is changing. + */ +public abstract class CompositeFieldQuickFix extends QuickFix { + protected Composite composite; + private int ordinal; + + /** + * Constructor + * @param program the program containing the composite. + * @param composite the composite being changed + * @param ordinal the ordinal of the field within the composite + * @param original the original name of the field + * @param newName the new name for the field + */ + public CompositeFieldQuickFix(Program program, Composite composite, int ordinal, + String original, String newName) { + super(program, original, newName); + this.composite = composite; + this.ordinal = ordinal; + } + + @Override + public Address getAddress() { + return null; + } + + @Override + public String getPath() { + return composite.getPathName(); + } + + protected DataTypeComponent findComponent(String name) { + DataTypeComponent component = getComponentByOrdinal(); + if (component != null) { + if (name.equals(component.getFieldName())) { + return component; + } + } + + // perhaps it moved (has a different ordinal now)? + DataTypeComponent[] components = composite.getDefinedComponents(); + for (int i = 0; i < components.length; i++) { + if (name.equals(components[i].getFieldName())) { + ordinal = i; + return components[i]; + } + } + return null; + } + + protected DataTypeComponent getComponentByOrdinal() { + if (composite.isDeleted()) { + return null; + } + if (ordinal >= composite.getNumComponents()) { + return null; + } + + return composite.getComponent(ordinal); + } + + @Override + public Map getCustomToolTipData() { + return Map.of("DataType", composite.getPathName()); + } + + @Override + protected boolean navigateSpecial(ServiceProvider services, boolean fromSelectionChange) { + DataTypeManagerService dtmService = services.getService(DataTypeManagerService.class); + if (dtmService == null) { + return false; + } + + dtmService.setDataTypeSelected(composite); + + if (!fromSelectionChange) { + dtmService.edit(composite, getFieldName()); + } + return true; + } + + protected abstract String getFieldName(); + + protected void editComposite(DataTypeManagerService dtmService) { + dtmService.edit(composite); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameCategoryQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameCategoryQuickFix.java new file mode 100644 index 0000000000..883a2a608f --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameCategoryQuickFix.java @@ -0,0 +1,121 @@ +/* ### + * 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.replace.items; + +import java.util.Map; + +import ghidra.app.services.DataTypeManagerService; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.features.base.replace.RenameQuickFix; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.Category; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.util.InvalidNameException; +import ghidra.util.exception.DuplicateNameException; + +/** + * QuickFix for renaming datatype categories. + */ +public class RenameCategoryQuickFix extends RenameQuickFix { + + private Category category; + + /** + * Constructor + * @param program the program containing the category to be renamed + * @param category the category to be renamed + * @param newName the new name for the category + */ + public RenameCategoryQuickFix(Program program, Category category, String newName) { + super(program, category.getName(), newName); + this.category = category; + checkForDuplicates(); + } + + private void checkForDuplicates() { + Category parent = category.getParent(); + if (parent == null) { + return; + } + if (parent.getCategory(replacement) != null) { + setStatus(QuickFixStatus.WARNING, + "The name \"" + replacement + "\" already exists in category \"" + + parent.getCategoryPathName() + "\""); + } + } + + @Override + public void statusChanged(QuickFixStatus newStatus) { + if (newStatus == QuickFixStatus.NONE) { + checkForDuplicates(); + } + } + + @Override + public String getItemType() { + return "datatype category"; + } + + @Override + public Address getAddress() { + return null; + } + + @Override + public String getPath() { + return category.getParent().getCategoryPathName(); + } + + @Override + protected String doGetCurrent() { + return category.getName(); + } + + @Override + protected void execute() { + try { + category.setName(replacement); + } + catch (DuplicateNameException | InvalidNameException e) { + setStatus(QuickFixStatus.ERROR, "Rename Failed! " + e.getMessage()); + } + + } + + @Override + public ProgramLocation getProgramLocation() { + return null; + } + + @Override + protected boolean navigateSpecial(ServiceProvider services, boolean fromSelectionChange) { + DataTypeManagerService dtmService = services.getService(DataTypeManagerService.class); + if (dtmService == null) { + return false; + } + + dtmService.setCategorySelected(category); + return true; + } + + @Override + public Map getCustomToolTipData() { + return Map.of("Parent Path", category.getParent().getCategoryPathName()); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameDataTypeQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameDataTypeQuickFix.java new file mode 100644 index 0000000000..5d758de007 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameDataTypeQuickFix.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.replace.items; + +import java.util.Map; + +import ghidra.app.services.DataTypeManagerService; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.features.base.replace.RenameQuickFix; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.*; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; + +/** + * QuickFix for renaming datatypes. + */ +public class RenameDataTypeQuickFix extends RenameQuickFix { + + private DataType dataType; + + /** + * Constructor + * @param program the program containing the datatype to be renamed + * @param dataType the datatype being renamed + * @param newName the new name for the datatype + */ + public RenameDataTypeQuickFix(Program program, DataType dataType, String newName) { + super(program, dataType.getName(), newName); + this.dataType = dataType; + if (!canRename()) { + setStatus(QuickFixStatus.ERROR, "This datatype doesn't support renaming"); + } + checkDuplicate(); + } + + private void checkDuplicate() { + CategoryPath categoryPath = dataType.getCategoryPath(); + DataTypeManager dtm = dataType.getDataTypeManager(); + Category category = dtm.getCategory(categoryPath); + DataType existing = category.getDataType(replacement); + if (existing != null) { + setStatus(QuickFixStatus.WARNING, "Datatype with name \"" + replacement + + "\" already exists in category \"" + category.getCategoryPathName() + "\""); + } + } + + @Override + public String getItemType() { + return "Datatype"; + } + + @Override + public Address getAddress() { + return null; + } + + private boolean canRename() { + return !(dataType instanceof BuiltInDataType || + dataType instanceof MissingBuiltInDataType || dataType instanceof Array || + dataType instanceof Pointer); + } + + @Override + public String getPath() { + return dataType.getCategoryPath().getPath(); + } + + @Override + public String doGetCurrent() { + if (dataType.isDeleted()) { + return null; + } + return dataType.getName(); + } + + @Override + public void execute() { + try { + dataType.setName(replacement); + } + catch (Exception e) { + setStatus(QuickFixStatus.ERROR, "Rename datatype failed: " + e.getMessage()); + } + } + + @Override + public ProgramLocation getProgramLocation() { + return null; + } + + @Override + protected boolean navigateSpecial(ServiceProvider services, boolean fromSelectionChange) { + DataTypeManagerService dtmService = services.getService(DataTypeManagerService.class); + if (dtmService == null) { + return false; + } + + dtmService.setDataTypeSelected(dataType); + + if (!fromSelectionChange) { + dtmService.edit(dataType); + } + return true; + } + + @Override + public Map getCustomToolTipData() { + return Map.of("Category", dataType.getCategoryPath().toString()); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameEnumValueQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameEnumValueQuickFix.java new file mode 100644 index 0000000000..6f657471ca --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameEnumValueQuickFix.java @@ -0,0 +1,127 @@ +/* ### + * 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.replace.items; + +import java.util.Map; + +import ghidra.app.services.DataTypeManagerService; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.features.base.replace.RenameQuickFix; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.Enum; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; + +/** + * QuickFix for renaming enum values. + */ +public class RenameEnumValueQuickFix extends RenameQuickFix { + + private Enum enumm; + private long enumValue; + + /** + * Constructor + * @param program the program containing the enum to be renamed + * @param enumDt the enum whose value is being renamed + * @param valueName the enum value name being changed + * @param newName the new name for the enum value + */ + public RenameEnumValueQuickFix(Program program, Enum enumDt, String valueName, + String newName) { + super(program, valueName, newName); + this.enumm = enumDt; + this.enumValue = enumDt.getValue(valueName); + validate(); + } + + @Override + public String getItemType() { + return "Enum Value"; + } + + private void validate() { + if (enumm.contains(replacement)) { + setStatus(QuickFixStatus.WARNING, + "New name not allowed because it duplicates an existing value name"); + } + } + + @Override + protected void statusChanged(QuickFixStatus newStatus) { + if (newStatus == QuickFixStatus.NONE) { + validate(); + } + } + + @Override + public Address getAddress() { + return null; + } + + @Override + public String getPath() { + return enumm.getPathName(); + } + + @Override + public String doGetCurrent() { + if (enumm.contains(original)) { + return original; + } + else if (enumm.contains(replacement)) { + return replacement; + } + return null; + } + + @Override + public void execute() { + try { + enumm.add(replacement, enumValue); + enumm.remove(original); + } + catch (Exception e) { + setStatus(QuickFixStatus.ERROR, "Rename enum value failed: " + e.getMessage()); + } + } + + @Override + public ProgramLocation getProgramLocation() { + return null; + } + + @Override + public Map getCustomToolTipData() { + return Map.of("Enum", enumm.getPathName()); + } + + @Override + protected boolean navigateSpecial(ServiceProvider services, boolean fromSelectionChange) { + DataTypeManagerService dtmService = services.getService(DataTypeManagerService.class); + if (dtmService == null) { + return false; + } + + dtmService.setDataTypeSelected(enumm); + + if (!fromSelectionChange) { + dtmService.edit(enumm); + } + return true; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameFieldQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameFieldQuickFix.java new file mode 100644 index 0000000000..3674bea9f5 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameFieldQuickFix.java @@ -0,0 +1,89 @@ +/* ### + * 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.replace.items; + +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.program.model.data.Composite; +import ghidra.program.model.data.DataTypeComponent; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; + +/** + * QuickFix for renaming structure or union fields + */ +public class RenameFieldQuickFix extends CompositeFieldQuickFix { + + /** + * Constructor + * @param program the program containing the structure or union field to be renamed + * @param composite the composite whose field is being renamed + * @param ordinal the ordinal of the field being renamed with its containing composite + * @param original the original name of the field + * @param newName the new name for the enum value + */ + public RenameFieldQuickFix(Program program, Composite composite, int ordinal, String original, + String newName) { + super(program, composite, ordinal, original, newName); + } + + @Override + public String getActionName() { + return "Rename"; + } + + @Override + public String getItemType() { + return "Field Name"; + } + + @Override + public String doGetCurrent() { + DataTypeComponent component = getComponent(); + return component == null ? null : component.getFieldName(); + } + + private DataTypeComponent getComponent() { + DataTypeComponent component = findComponent(original); + if (component == null) { + component = findComponent(replacement); + } + return component; + } + + @Override + public void execute() { + try { + DataTypeComponent component = getComponent(); + if (component != null) { + component.setFieldName(replacement); + } + } + catch (Exception e) { + setStatus(QuickFixStatus.ERROR, "Rename field failed: " + e.getMessage()); + } + } + + @Override + public ProgramLocation getProgramLocation() { + return null; + } + + @Override + protected String getFieldName() { + return current; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameMemoryBlockQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameMemoryBlockQuickFix.java new file mode 100644 index 0000000000..117e7feb94 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameMemoryBlockQuickFix.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.replace.items; + +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.features.base.replace.RenameQuickFix; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.util.MemoryBlockStartFieldLocation; +import ghidra.program.util.ProgramLocation; + +/** + * QuickFix for renaming memory blocks. + */ +public class RenameMemoryBlockQuickFix extends RenameQuickFix { + + private MemoryBlock block; + + /** + * Constructor + * @param program the program containing the memory block to be renamed + * @param block the memory block to be renamed + * @param newName the new name for the memory block + */ + public RenameMemoryBlockQuickFix(Program program, MemoryBlock block, String newName) { + super(program, block.getName(), newName); + this.block = block; + } + + @Override + public String getItemType() { + return "Memory Block"; + } + + @Override + public String doGetCurrent() { + return block.getName(); + } + + @Override + public void execute() { + try { + block.setName(replacement); + } + catch (Exception e) { + setStatus(QuickFixStatus.ERROR, "Rename Failed! " + e); + } + } + + @Override + public Address getAddress() { + return block.getStart(); + } + + @Override + public String getPath() { + return null; + } + + @Override + public ProgramLocation getProgramLocation() { + return new MemoryBlockStartFieldLocation(program, getAddress(), null, 0, 0, null, 0); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameProgramTreeGroupQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameProgramTreeGroupQuickFix.java new file mode 100644 index 0000000000..a2b594087b --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameProgramTreeGroupQuickFix.java @@ -0,0 +1,144 @@ +/* ### + * 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.replace.items; + +import java.util.Map; + +import ghidra.app.services.ProgramTreeService; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.features.base.replace.RenameQuickFix; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.*; +import ghidra.program.util.ProgramLocation; + +/** + * QuickFix for renaming program tree groups (modules or fragments) + */ +public class RenameProgramTreeGroupQuickFix extends RenameQuickFix { + + private String path; + private Group group; + + /** + * Constructor + * @param program the program containing the program tree group to be renamed + * @param group the program tree module or fragment to be renamed + * @param newName the new name for the memory block + */ + public RenameProgramTreeGroupQuickFix(Program program, Group group, + String newName) { + super(program, group.getName(), newName); + this.group = group; + this.path = computePath(); + checkForDuplicates(); + } + + private void checkForDuplicates() { + ProgramModule[] parents = group.getParents(); + if (parents != null) { + for (ProgramModule module : parents) { + if (module.getIndex(replacement) >= 0) { + setStatus(QuickFixStatus.WARNING, + "The name \"" + replacement + "\" already exists in module \"" + + module.getName() + "\""); + } + } + } + } + + @Override + public void statusChanged(QuickFixStatus newStatus) { + if (newStatus == QuickFixStatus.NONE) { + checkForDuplicates(); + } + } + + @Override + public String getItemType() { + if (group instanceof ProgramFragment) { + return "Program Tree Fragment"; + } + return "Program Tree Module"; + } + + private String computePath() { + StringBuilder builder = new StringBuilder(); + computePath(group, builder); + return builder.toString(); + } + + private void computePath(Group treeGroup, StringBuilder builder) { + ProgramModule[] parents = treeGroup.getParents(); + if (parents.length > 0) { + computePath(parents[0], builder); + builder.append("/"); + } + builder.append(treeGroup.getName()); + } + + @Override + public String getPath() { + return path; + } + + @Override + protected String doGetCurrent() { + if (group.isDeleted()) { + return null; + } + return group.getName(); + } + + @Override + protected void execute() { + try { + group.setName(replacement); + } + catch (Exception e) { + setStatus(QuickFixStatus.ERROR, "Rename Failed! " + e.getMessage()); + } + } + + @Override + public ProgramLocation getProgramLocation() { + if (getAddress() != null) { + return new ProgramLocation(program, getAddress()); + } + return null; + } + + @Override + public Address getAddress() { + return group.getMinAddress(); + } + + @Override + protected boolean navigateSpecial(ServiceProvider services, boolean fromSelectionChange) { + ProgramTreeService service = services.getService(ProgramTreeService.class); + if (service == null) { + return false; + } + service.setViewedTree(group.getTreeName()); + service.setGroupSelection(group.getGroupPath()); + return true; + } + + @Override + public Map getCustomToolTipData() { + return Map.of("Program Tree Path", path); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameSymbolQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameSymbolQuickFix.java new file mode 100644 index 0000000000..8dfaf0f694 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/RenameSymbolQuickFix.java @@ -0,0 +1,136 @@ +/* ### + * 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.replace.items; + +import java.util.List; +import java.util.Map; + +import ghidra.app.plugin.core.symboltree.SymbolTreeService; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.features.base.replace.RenameQuickFix; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.symbol.*; +import ghidra.program.util.ProgramLocation; +import ghidra.util.exception.DuplicateNameException; +import ghidra.util.exception.InvalidInputException; + +/** + * QuickFix for renaming symbols (labels, functions, namespaces, classes, parameters, or + * local variables). + */ +public class RenameSymbolQuickFix extends RenameQuickFix { + + private Symbol symbol; + + /** + * Constructor + * @param symbol the symbol to be renamed + * @param newName the new name for the symbol + */ + public RenameSymbolQuickFix(Symbol symbol, String newName) { + super(symbol.getProgram(), symbol.getName(), newName); + this.symbol = symbol; + performDuplicateCheck(); + } + + @Override + public String getItemType() { + return symbol.getSymbolType().toString(); + } + + private void performDuplicateCheck() { + Namespace parentNamespace = symbol.getParentNamespace(); + SymbolTable symbolTable = program.getSymbolTable(); + List symbols = symbolTable.getSymbols(replacement, parentNamespace); + if (!symbols.isEmpty()) { + setStatus(QuickFixStatus.WARNING, + "There is already a symbol named \"" + replacement + + "\" in namespace \"" + parentNamespace.getName() + "\""); + } + } + + @Override + public Address getAddress() { + Address address = symbol.getAddress(); + if (address == Address.NO_ADDRESS) { + address = null; + } + return address; + } + + @Override + public void statusChanged(QuickFixStatus newStatus) { + if (newStatus == QuickFixStatus.NONE) { + performDuplicateCheck(); + } + } + + @Override + public ProgramLocation getProgramLocation() { + return symbol.getProgramLocation(); + } + + @Override + public String getPath() { + Namespace namespace = symbol.getParentNamespace(); + return namespace.getName(true); + } + + @Override + public String doGetCurrent() { + if (symbol.isDeleted()) { + return null; + } + return symbol.getName(); + } + + @Override + public void execute() { + try { + symbol.setName(replacement, SourceType.USER_DEFINED); + } + catch (DuplicateNameException | InvalidInputException e) { + setStatus(QuickFixStatus.ERROR, "Rename Failed! " + e.getMessage()); + } + } + + @Override + public Map getCustomToolTipData() { + SymbolType symbolType = symbol.getSymbolType(); + Namespace parentNamespace = symbol.getParentNamespace(); + if (symbolType == SymbolType.PARAMETER || symbolType == SymbolType.LOCAL_VAR) { + return Map.of("Function", parentNamespace.getName(true)); + } + return Map.of("Namespace", parentNamespace.getName(false)); + } + + @Override + protected boolean navigateSpecial(ServiceProvider services, boolean fromSelectionChange) { + if (symbol.getAddress().isMemoryAddress()) { + return false; // let default navigation handle it + } + + // This is a symbol that can't be shown in the listing, so directly request the + // symbol tree to select this symbol + SymbolTreeService service = services.getService(SymbolTreeService.class); + if (service != null) { + service.selectSymbol(symbol); + return true; + } + return false; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateCommentQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateCommentQuickFix.java new file mode 100644 index 0000000000..b15f03480e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateCommentQuickFix.java @@ -0,0 +1,96 @@ +/* ### + * 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.replace.items; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.CommentType; +import ghidra.program.model.listing.Program; +import ghidra.program.util.*; + +/** + * QuickFix for updating listing comments. + */ +public class UpdateCommentQuickFix extends QuickFix { + + private Address address; + private CommentType type; + + /** + * Constructor + * @param program the program containing the comment to be renamed + * @param address The address where the comment is located + * @param type the type of comment (Pre, Post, EOL, etc.) + * @param comment the original comment text + * @param newComment the new comment text + */ + public UpdateCommentQuickFix(Program program, Address address, CommentType type, String comment, + String newComment) { + + super(program, comment, newComment); + this.address = address; + this.type = type; + } + + @Override + public String getActionName() { + return "Update"; + } + + @Override + public String getItemType() { + return "Code Comment"; + } + + @Override + public String doGetCurrent() { + return program.getListing().getComment(type, address); + } + + @Override + public void execute() { + program.getListing().setComment(address, type, replacement); + } + + @Override + public Address getAddress() { + return address; + } + + @Override + public ProgramLocation getProgramLocation() { + switch (type) { + case EOL: + return new EolCommentFieldLocation(program, address, null, null, 0, 0, 0); + case PLATE: + return new PlateFieldLocation(program, address, null, 0, 0, null, 0); + case POST: + return new PostCommentFieldLocation(program, address, null, null, 0, 0); + case PRE: + return new CommentFieldLocation(program, address, null, null, type.ordinal(), 0, 0); + case REPEATABLE: + return new RepeatableCommentFieldLocation(program, address, null, null, 0, 0, 0); + default: + return null; + } + } + + @Override + public String getPath() { + return null; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateDataTypeDescriptionQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateDataTypeDescriptionQuickFix.java new file mode 100644 index 0000000000..d7d1b9d8ae --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateDataTypeDescriptionQuickFix.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.features.base.replace.items; + +import java.util.Map; + +import ghidra.app.services.DataTypeManagerService; +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.Composite; +import ghidra.program.model.data.DataType; +import ghidra.program.model.data.Enum; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; + +/** + * QuickFix for updating a datatype's description (Only supported on structures, unions, or enums) + */ +public class UpdateDataTypeDescriptionQuickFix extends QuickFix { + + private DataType dataType; + + /** + * Constructor + * @param program the program containing the datatype description to be updated. + * @param dataType the datatype being renamed + * @param newDescription the new name for the datatype + */ + public UpdateDataTypeDescriptionQuickFix(Program program, DataType dataType, + String newDescription) { + super(program, getDescription(dataType), newDescription); + this.dataType = dataType; + } + + @Override + public String getItemType() { + return "Datatype Description"; + } + + @Override + public Address getAddress() { + return null; + } + + @Override + public String getPath() { + return dataType.getCategoryPath().getPath(); + } + + @Override + public String doGetCurrent() { + if (dataType.isDeleted()) { + return null; + } + return getDescription(dataType); + } + + private static String getDescription(DataType dt) { + if (dt instanceof Composite composite) { + return composite.getDescription(); + } + if (dt instanceof Enum enumDataType) { + return enumDataType.getDescription(); + } + return null; + + } + + @Override + public void execute() { + try { + if (dataType instanceof Composite composite) { + composite.setDescription(replacement); + } + else if (dataType instanceof Enum enumDataType) { + enumDataType.setDescription(replacement); + } + } + catch (Exception e) { + setStatus(QuickFixStatus.ERROR, "Rename datatype failed: " + e.getMessage()); + } + } + + @Override + public ProgramLocation getProgramLocation() { + return null; + } + + @Override + protected boolean navigateSpecial(ServiceProvider services, boolean fromSelectionChange) { + DataTypeManagerService dtmService = services.getService(DataTypeManagerService.class); + if (dtmService == null) { + return false; + } + + dtmService.setDataTypeSelected(dataType); + + if (!fromSelectionChange) { + dtmService.edit(dataType); + } + return true; + } + + @Override + public Map getCustomToolTipData() { + return Map.of("Category", dataType.getCategoryPath().toString()); + } + + @Override + public String getActionName() { + return "Update"; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateEnumCommentQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateEnumCommentQuickFix.java new file mode 100644 index 0000000000..2c320f5a09 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateEnumCommentQuickFix.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.features.base.replace.items; + +import java.util.Map; + +import ghidra.app.services.DataTypeManagerService; +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.Enum; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; + +/** + * QuickFix for updating enum value comments + */ +public class UpdateEnumCommentQuickFix extends QuickFix { + + private Enum enumm; + private String valueName; + + /** + * Constructor + * @param program the program containing the enum value whose comment is to be updated + * @param enumDt the enum whose field value comment is to be changed + * @param valueName the enum value name whose comment is to be changed + * @param newComment the new comment for the enum value + */ + public UpdateEnumCommentQuickFix(Program program, Enum enumDt, String valueName, + String newComment) { + super(program, enumDt.getComment(valueName), newComment); + this.enumm = enumDt; + this.valueName = valueName; + } + + @Override + public String getActionName() { + return "Update"; + } + + @Override + public String getItemType() { + return "Enum Comment"; + } + + @Override + public Address getAddress() { + return null; + } + + @Override + public String getPath() { + return enumm.getCategoryPath().getPath(); + } + + @Override + public String doGetCurrent() { + return enumm.getComment(valueName); + } + + @Override + public void execute() { + try { + long value = enumm.getValue(valueName); + enumm.remove(valueName); + enumm.add(valueName, value, replacement); + } + catch (Exception e) { + setStatus(QuickFixStatus.ERROR, "Update enum comment failed: " + e.getMessage()); + } + } + + @Override + public ProgramLocation getProgramLocation() { + return null; + } + + @Override + protected boolean navigateSpecial(ServiceProvider services, boolean fromSelectionChange) { + DataTypeManagerService dtmService = services.getService(DataTypeManagerService.class); + if (dtmService == null) { + return false; + } + + dtmService.setDataTypeSelected(enumm); + + if (!fromSelectionChange) { + dtmService.edit(enumm); + } + return true; + } + + @Override + public Map getCustomToolTipData() { + return Map.of("Datatype", enumm.getPathName()); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateFieldCommentQuickFix.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateFieldCommentQuickFix.java new file mode 100644 index 0000000000..9565e2ff96 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/replace/items/UpdateFieldCommentQuickFix.java @@ -0,0 +1,82 @@ +/* ### + * 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.replace.items; + +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.program.model.data.Composite; +import ghidra.program.model.data.DataTypeComponent; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; + +/** + * QuickFix for updating structure or union field comments + */ +public class UpdateFieldCommentQuickFix extends CompositeFieldQuickFix { + private String fieldName; + + /** + * Constructor + * @param program the program containing the enum value whose comment is to be updated + * @param composite the structure or union whose field comment is to be changed + * @param fieldName the field name whose comment is to be changed + * @param ordinal the ordinal of the field being renamed with its containing composite + * @param original the original comment of the field + * @param newComment the new comment for the field + */ + public UpdateFieldCommentQuickFix(Program program, Composite composite, String fieldName, + int ordinal, String original, String newComment) { + super(program, composite, ordinal, original, newComment); + this.fieldName = fieldName; + } + + @Override + public String getActionName() { + return "Update"; + } + + @Override + public String getItemType() { + return "Field Comment"; + } + + @Override + public String doGetCurrent() { + DataTypeComponent component = findComponent(fieldName); + return component == null ? null : component.getComment(); + } + + @Override + public void execute() { + DataTypeComponent component = findComponent(fieldName); + try { + component.setComment(replacement); + } + catch (Exception e) { + setStatus(QuickFixStatus.ERROR, "Update field comment failed: " + e.getMessage()); + } + } + + @Override + public ProgramLocation getProgramLocation() { + return null; + } + + @Override + protected String getFieldName() { + return fieldName; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/program/database/ProgramBuilder.java b/Ghidra/Features/Base/src/main/java/ghidra/program/database/ProgramBuilder.java index 7c50ee6037..155fd8d4f3 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/program/database/ProgramBuilder.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/program/database/ProgramBuilder.java @@ -639,7 +639,7 @@ public class ProgramBuilder { }); } - public Namespace createClassNamespace(String name, String parentNamespace, SourceType type) + public GhidraClass createClassNamespace(String name, String parentNamespace, SourceType type) throws Exception { return tx(() -> { Namespace ns = getNamespace(parentNamespace); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/table/GhidraTable.java b/Ghidra/Features/Base/src/main/java/ghidra/util/table/GhidraTable.java index d394031f5b..a9bb0f745e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/util/table/GhidraTable.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/table/GhidraTable.java @@ -276,7 +276,7 @@ public class GhidraTable extends GTable { * This method differs from {@link #navigate(int, int)} in that this method will not * navigate if {@link #navigateOnSelection} is false. */ - private void navigateOnCurrentSelection(int row, int column) { + protected void navigateOnCurrentSelection(int row, int column) { if (!navigateOnSelection) { return; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/table/actions/DeleteTableRowAction.java b/Ghidra/Features/Base/src/main/java/ghidra/util/table/actions/DeleteTableRowAction.java index 5819db0c25..823bf73526 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/util/table/actions/DeleteTableRowAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/table/actions/DeleteTableRowAction.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. @@ -72,17 +72,22 @@ public class DeleteTableRowAction extends DockingAction { } public DeleteTableRowAction(GTable table, String owner) { - this(NAME, owner, DEFAULT_KEYSTROKE); + this(table, owner, null); + } + + public DeleteTableRowAction(GTable table, String owner, String menuGroup) { + this(NAME, owner, DEFAULT_KEYSTROKE, menuGroup); this.table = table; } - private DeleteTableRowAction(String name, String owner, KeyStroke defaultkeyStroke) { + private DeleteTableRowAction(String name, String owner, KeyStroke defaultkeyStroke, + String menuGroup) { super(name, owner, KeyBindingType.SHARED); setDescription("Remove the selected rows from the table"); setHelpLocation(new HelpLocation(HelpTopics.SEARCH, "Remove_Items")); setToolBarData(new ToolBarData(ICON, null)); - setPopupMenuData(new MenuData(new String[] { "Remove Items" }, ICON, null)); + setPopupMenuData(new MenuData(new String[] { "Remove Items" }, ICON, menuGroup)); initKeyStroke(defaultkeyStroke); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/table/actions/MakeProgramSelectionAction.java b/Ghidra/Features/Base/src/main/java/ghidra/util/table/actions/MakeProgramSelectionAction.java index 60d060870e..7c09aecbcb 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/util/table/actions/MakeProgramSelectionAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/table/actions/MakeProgramSelectionAction.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. @@ -62,7 +62,7 @@ public class MakeProgramSelectionAction extends DockingAction { public MakeProgramSelectionAction(String owner, GhidraTable table) { super("Make Selection", owner, KeyBindingType.SHARED); this.table = Objects.requireNonNull(table); - init(); + init(null); } /** @@ -74,10 +74,24 @@ public class MakeProgramSelectionAction extends DockingAction { * @param table the table needed for this action */ public MakeProgramSelectionAction(Navigatable navigatable, String owner, GhidraTable table) { + this(navigatable, owner, table, null); + } + + /** + * Special constructor for clients that do not have a plugin. Clients using this + * constructor must override {@link #makeProgramSelection(ProgramSelection, ActionContext)}. + * + * @param navigatable the navigatable that will be used to make selections; may not be null + * @param owner the action's owner + * @param table the table needed for this action + * @param menuGroup The popup menu group for this action + */ + public MakeProgramSelectionAction(Navigatable navigatable, String owner, GhidraTable table, + String menuGroup) { super("Make Selection", owner, KeyBindingType.SHARED); this.navigatable = Objects.requireNonNull(navigatable); this.table = Objects.requireNonNull(table); - init(); + init(menuGroup); } /** @@ -91,12 +105,12 @@ public class MakeProgramSelectionAction extends DockingAction { super("Make Selection", plugin.getName(), KeyBindingType.SHARED); this.plugin = Objects.requireNonNull(plugin); this.table = Objects.requireNonNull(table); - init(); + init(null); } - private void init() { + private void init(String menuGroup) { setPopupMenuData( - new MenuData(new String[] { "Make Selection" }, Icons.MAKE_SELECTION_ICON)); + new MenuData(new String[] { "Make Selection" }, Icons.MAKE_SELECTION_ICON, menuGroup)); setToolBarData(new ToolBarData(Icons.MAKE_SELECTION_ICON)); setDescription("Make a program selection from the selected rows"); diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/services/TestDoubleDataTypeManagerService.java b/Ghidra/Features/Base/src/test/java/ghidra/app/services/TestDoubleDataTypeManagerService.java index 263885c832..2894899d7f 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/app/services/TestDoubleDataTypeManagerService.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/services/TestDoubleDataTypeManagerService.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. @@ -100,7 +100,7 @@ public class TestDoubleDataTypeManagerService implements DataTypeManagerService } @Override - public void edit(Structure structure, String fieldName) { + public void edit(Composite composite, String fieldName) { throw new UnsupportedOperationException(); } @@ -131,6 +131,11 @@ public class TestDoubleDataTypeManagerService implements DataTypeManagerService throw new UnsupportedOperationException(); } + @Override + public void setCategorySelected(Category category) { + throw new UnsupportedOperationException(); + } + @Override public List getSelectedDatatypes() { throw new UnsupportedOperationException(); diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/AbstractSearchAndReplaceTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/AbstractSearchAndReplaceTest.java new file mode 100644 index 0000000000..26069842c3 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/AbstractSearchAndReplaceTest.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.replace; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Before; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.model.symbol.*; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.util.datastruct.ListAccumulator; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.InvalidInputException; +import ghidra.util.task.TaskMonitor; + +/** + * Tests user input with search options for both one word searchers (renaming labels, datatypes, + * etc.) and updating multi-word matches (comments) + */ +public class AbstractSearchAndReplaceTest extends AbstractGhidraHeadedIntegrationTest { + protected static final boolean CASE_SENSITIVE_ON = true; + protected static final boolean CASE_SENSITIVE_OFF = false; + protected static final boolean WHOLE_WORD_ON = true; + protected static final boolean WHOLE_WORD_OFF = false; + protected static final int SEARCH_LIMIT = 100; + protected Program program; + protected ProgramBuilder builder; + private Set querySearchTypes = new HashSet<>(); + protected SearchType labels; + protected SearchType functions; + protected SearchType namespaces; + protected SearchType classes; + protected SearchType parameters; + protected SearchType localVariables; + protected SearchType comments; + protected SearchType memoryBlocks; + protected SearchType dataTypes; + protected SearchType dataTypeComments; + protected SearchType fieldNames; + protected SearchType enumValues; + protected SearchType programTrees; + protected SearchType categories; + + @Before + public void setUp() throws Exception { + program = buildProgram(); + + Map typesMap = gatherSearchTypes(); + + labels = typesMap.get("Labels"); + namespaces = typesMap.get("Namespaces"); + functions = typesMap.get("Functions"); + classes = typesMap.get("Classes"); + parameters = typesMap.get("Parameters"); + localVariables = typesMap.get("Local Variables"); + comments = typesMap.get("Comments"); + memoryBlocks = typesMap.get("Memory Blocks"); + dataTypes = typesMap.get("Datatypes"); + dataTypeComments = typesMap.get("Datatype Comments"); + fieldNames = typesMap.get("Datatype Fields"); + enumValues = typesMap.get("Enum Values"); + programTrees = typesMap.get("Program Trees"); + categories = typesMap.get("Datatype Categories"); + } + + private Map gatherSearchTypes() { + Set searchTypes = SearchType.getSearchTypes(); + Map typesMap = new HashMap<>(); + for (SearchType searchType : searchTypes) { + typesMap.put(searchType.getName(), searchType); + } + return typesMap; + } + + protected Program buildProgram() throws Exception { + builder = new ProgramBuilder("TestX86", ProgramBuilder._X86); + builder.createMemory(".text", Long.toHexString(000), 0x1000); + return builder.getProgram(); + } + + protected void assertQuickFix(long address, String original, String preview, QuickFix item) { + assertQuickFix(addr(address), original, preview, item); + } + + protected void assertQuickFix(String original, String preview, QuickFix item) { + assertEquals(original, item.getOriginal()); + assertEquals(preview, item.getPreview()); + } + + protected void assertQuickFix(Address address, String original, String preview, QuickFix item) { + assertEquals(address, item.getAddress()); + assertEquals(original, item.getOriginal()); + assertEquals(preview, item.getPreview()); + } + + protected Address addr(long address) { + return program.getAddressFactory().getDefaultAddressSpace().getAddress(address); + } + + protected List queryRegEx(String search, String replace, boolean caseSensitive) + throws CancelledException { + return query(search, replace, true, caseSensitive, false); + } + + protected List query(String search, String replace, boolean caseSensitive, + boolean wholeWord) throws CancelledException { + return query(search, replace, false, caseSensitive, wholeWord); + } + + private List query(String search, String replace, boolean isRegEx, + boolean isCaseSensitive, boolean isWholeWord) throws CancelledException { + SearchAndReplaceQuery query = + new SearchAndReplaceQuery(search, replace, querySearchTypes, isRegEx, isCaseSensitive, + isWholeWord, SEARCH_LIMIT); + ListAccumulator accumulator = new ListAccumulator<>(); + query.findAll(program, accumulator, TaskMonitor.DUMMY); + return accumulator.asList(); + } + + protected Symbol createLabel(long address, String name) { + return builder.createLabel(Long.toHexString(address), name); + } + + protected Function createFunction(long address, String name, String... paramNames) + throws Exception { + + Parameter[] params = createParameters(paramNames); + return builder.createEmptyFunction(name, Long.toHexString(address), 1, + new Integer16DataType(), params); + } + + private Parameter[] createParameters(String[] paramNames) throws InvalidInputException { + Parameter[] params = new Parameter[paramNames.length]; + for (int i = 0; i < paramNames.length; i++) { + params[i] = new ParameterImpl(paramNames[i], new ByteDataType(), program); + } + return params; + } + + protected Namespace createNamespace(Namespace parent, String name) { + return builder.createNamespace(name, parent.getName(), SourceType.USER_DEFINED); + } + + protected MemoryBlock createBlock(String name, int address) { + return builder.createMemory(name, Long.toHexString(address), 10); + } + + protected GhidraClass createClass(Namespace parent, String name) throws Exception { + return builder.createClassNamespace(name, parent.getName(), SourceType.USER_DEFINED); + } + + protected void createComment(long address, CommentType commentType, String comment) { + builder.createComment(Long.toHexString(address), comment, commentType.ordinal()); + } + + protected void setSearchTypes(SearchType... searchTypes) { + querySearchTypes.clear(); + for (SearchType searchType : searchTypes) { + querySearchTypes.add(searchType); + } + } + + protected void sortByAddress(List results) { + Collections.sort(results, (a, b) -> a.getAddress().compareTo(b.getAddress())); + } + + protected void sortByName(List results) { + Collections.sort(results, (a, b) -> a.getOriginal().compareTo(b.getOriginal())); + } + + protected void performAction(QuickFix item) { + program.withTransaction("test", () -> item.performAction()); + } + + protected DataType addDataType(DataType dt) { + DataTypeManager dtm = program.getDataTypeManager(); + return program.withTransaction("test", () -> dtm.addDataType(dt, null)); + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/CategoriesSearchAndReplaceTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/CategoriesSearchAndReplaceTest.java new file mode 100644 index 0000000000..f03c92c7e1 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/CategoriesSearchAndReplaceTest.java @@ -0,0 +1,95 @@ +/* ### + * 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.replace; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.program.model.data.CategoryPath; + +public class CategoriesSearchAndReplaceTest extends AbstractSearchAndReplaceTest { + @Test + public void testSearchCategories() throws Exception { + builder.addCategory(new CategoryPath("/abc/foo1")); + builder.addCategory(new CategoryPath("/foo2/xxx")); + builder.addCategory(new CategoryPath("/abc/xxfoo3xx")); + + setSearchTypes(categories); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(3, results.size()); + sortByName(results); + + assertQuickFix("foo1", "bar1", results.get(0)); + assertQuickFix("foo2", "bar2", results.get(1)); + assertQuickFix("xxfoo3xx", "xxbar3xx", results.get(2)); + } + + @Test + public void testRenamingCategory() throws Exception { + builder.addCategory(new CategoryPath("/abc/foo/def")); + + setSearchTypes(categories); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("datatype category", item.getItemType()); + assertEquals(null, item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + assertNotNull(program.getDataTypeManager().getCategory(new CategoryPath("/abc/bar"))); + } + + @Test + public void testRenameCategoryDuplicate() throws Exception { + builder.addCategory(new CategoryPath("/abc/foo")); + builder.addCategory(new CategoryPath("/abc/bar")); + + setSearchTypes(categories); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.WARNING, item.getStatus()); + assertEquals("The name \"bar\" already exists in category \"/abc\"", + item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("datatype category", item.getItemType()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.ERROR, item.getStatus()); + assertEquals("Rename Failed! Category named bar already exists", item.getStatusMessage()); + assertEquals("foo", item.getCurrent()); + assertNotNull(program.getDataTypeManager().getCategory(new CategoryPath("/abc/foo"))); + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/DataTypesSearchAndReplaceTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/DataTypesSearchAndReplaceTest.java new file mode 100644 index 0000000000..eece5ffd61 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/DataTypesSearchAndReplaceTest.java @@ -0,0 +1,454 @@ +/* ### + * 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.replace; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.program.model.data.*; +import ghidra.program.model.data.Enum; + +public class DataTypesSearchAndReplaceTest extends AbstractSearchAndReplaceTest { + @Test + public void testSearchDataTypes() throws Exception { + addDataType(new StructureDataType("fooStruct", 1)); + addDataType(new UnionDataType("fooUnion")); + addDataType(new EnumDataType("fooEnum", 4)); + addDataType(new TypedefDataType("fooTypeDef", new ByteDataType())); + + setSearchTypes(dataTypes); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(4, results.size()); + sortByName(results); + + assertQuickFix("fooEnum", "barEnum", results.get(0)); + assertQuickFix("fooStruct", "barStruct", results.get(1)); + assertQuickFix("fooTypeDef", "barTypeDef", results.get(2)); + assertQuickFix("fooUnion", "barUnion", results.get(3)); + } + + @Test + public void testRenamingDataType() throws Exception { + DataType dt = addDataType(new StructureDataType("fooStruct", 1)); + + setSearchTypes(dataTypes); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Datatype", item.getItemType()); + assertEquals(null, item.getProgramLocation()); + assertEquals("fooStruct", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("barStruct", item.getCurrent()); + assertEquals("barStruct", dt.getName()); + } + + @Test + public void testRenamingDataTypeDuplicate() throws Exception { + DataType dt = addDataType(new StructureDataType("fooStruct", 1)); + addDataType(new StructureDataType("barStruct", 1)); + + setSearchTypes(dataTypes); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.WARNING, item.getStatus()); + assertEquals("Datatype with name \"barStruct\" already exists in category \"/\"", + item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Datatype", item.getItemType()); + assertEquals(null, item.getProgramLocation()); + assertEquals("fooStruct", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.ERROR, item.getStatus()); + assertEquals( + "Rename datatype failed: DataType named barStruct already exists in category /", + item.getStatusMessage()); + assertEquals("fooStruct", item.getCurrent()); + assertEquals("fooStruct", dt.getName()); + } + + @Test + public void testDataTypeDescriptionsStruct() throws Exception { + StructureDataType struct = new StructureDataType("fooStruct", 1); + struct.setDescription("this is a foo description"); + + DataType structDB = addDataType(struct); + + setSearchTypes(dataTypeComments); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Update", item.getActionName()); + assertEquals("Datatype Description", item.getItemType()); + assertEquals(null, item.getProgramLocation()); + assertEquals("this is a foo description", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("this is a bar description", item.getCurrent()); + assertEquals("this is a bar description", structDB.getDescription()); + } + + @Test + public void testDataTypeDescriptionsEnum() throws Exception { + EnumDataType enum1 = new EnumDataType("enum1", 4); + enum1.add("aaa", 1); + enum1.add("bbb", 2); + enum1.setDescription("this is a foo description"); + + DataType enumDB = addDataType(enum1); + + setSearchTypes(dataTypeComments); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Update", item.getActionName()); + assertEquals("Datatype Description", item.getItemType()); + assertEquals(null, item.getProgramLocation()); + assertEquals("this is a foo description", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("this is a bar description", item.getCurrent()); + assertEquals("this is a bar description", enumDB.getDescription()); + } + + @Test + public void testSearchFieldNames() throws Exception { + StructureDataType dt1 = new StructureDataType("abc", 0); + dt1.add(new ByteDataType(), "fooStructField", null); + dt1.add(new ByteDataType(), "xxfooxxStructField", null); + + UnionDataType dt2 = new UnionDataType("abc"); + dt2.add(new ByteDataType(), "fooUnionField", null); + dt2.add(new ByteDataType(), "xxfooxxUnionField", null); + + addDataType(dt1); + addDataType(dt2); + + setSearchTypes(fieldNames); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(4, results.size()); + sortByName(results); + + assertQuickFix("fooStructField", "barStructField", results.get(0)); + assertQuickFix("fooUnionField", "barUnionField", results.get(1)); + assertQuickFix("xxfooxxStructField", "xxbarxxStructField", results.get(2)); + assertQuickFix("xxfooxxUnionField", "xxbarxxUnionField", results.get(3)); + } + + @Test + public void testRenameStructureFieldNames() throws Exception { + StructureDataType struct = new StructureDataType("abc", 0); + struct.add(new ByteDataType(), "fooStructField", null); + + Structure dt = (Structure) addDataType(struct); + + setSearchTypes(fieldNames); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Field Name", item.getItemType()); + assertEquals(null, item.getProgramLocation()); + assertEquals("fooStructField", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("barStructField", item.getCurrent()); + assertEquals("barStructField", dt.getComponent(0).getFieldName()); + + } + + @Test + public void testRenameUnionFieldNames() throws Exception { + UnionDataType union = new UnionDataType("abc"); + union.add(new ByteDataType(), "fooUnionField", null); + + Union dt = (Union) addDataType(union); + + setSearchTypes(fieldNames); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Field Name", item.getItemType()); + assertEquals(null, item.getProgramLocation()); + assertEquals("fooUnionField", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("barUnionField", item.getCurrent()); + assertEquals("barUnionField", dt.getComponent(0).getFieldName()); + + } + + @Test + public void testSearchFieldComments() throws Exception { + StructureDataType dt1 = new StructureDataType("abc", 0); + dt1.add(new ByteDataType(), "field1", "foo struct field1 comment"); + dt1.add(new ByteDataType(), "field2", "foo struct field2 comment"); + + UnionDataType dt2 = new UnionDataType("abc"); + dt2.add(new ByteDataType(), "field1", "foo union field1 comment"); + dt2.add(new ByteDataType(), "field2", "foo union field2 comment"); + + addDataType(dt1); + addDataType(dt2); + + setSearchTypes(dataTypeComments); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(4, results.size()); + sortByName(results); + + assertQuickFix("foo struct field1 comment", "bar struct field1 comment", results.get(0)); + assertQuickFix("foo struct field2 comment", "bar struct field2 comment", results.get(1)); + assertQuickFix("foo union field1 comment", "bar union field1 comment", results.get(2)); + assertQuickFix("foo union field2 comment", "bar union field2 comment", results.get(3)); + } + + @Test + public void testUpdateStructureFieldComments() throws Exception { + StructureDataType dt1 = new StructureDataType("abc", 0); + dt1.add(new ByteDataType(), "field1", "foo struct field1 comment"); + + Structure dt = (Structure) addDataType(dt1); + + setSearchTypes(dataTypeComments); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Update", item.getActionName()); + assertEquals("Field Comment", item.getItemType()); + assertEquals(null, item.getProgramLocation()); + assertEquals("foo struct field1 comment", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar struct field1 comment", item.getCurrent()); + assertEquals("bar struct field1 comment", dt.getComponent(0).getComment()); + + } + + @Test + public void testUpdateUnionFieldComments() throws Exception { + UnionDataType dt1 = new UnionDataType("abc"); + dt1.add(new ByteDataType(), "field1", "foo union field1 comment"); + + Union dt = (Union) addDataType(dt1); + + setSearchTypes(dataTypeComments); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Update", item.getActionName()); + assertEquals("Field Comment", item.getItemType()); + assertEquals(null, item.getProgramLocation()); + assertEquals("foo union field1 comment", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar union field1 comment", item.getCurrent()); + assertEquals("bar union field1 comment", dt.getComponent(0).getComment()); + + } + + @Test + public void testSearchEnumValueNames() throws Exception { + EnumDataType enum1 = new EnumDataType("enum1", 4); + enum1.add("foo", 1); + enum1.add("xxxfoo", 2); + + EnumDataType enum2 = new EnumDataType("enum2", 4); + enum1.add("fooEnum2", 1); + enum1.add("xxxfooEnum2", 2); + + addDataType(enum1); + addDataType(enum2); + + setSearchTypes(enumValues); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(4, results.size()); + sortByName(results); + + assertQuickFix("foo", "bar", results.get(0)); + assertQuickFix("fooEnum2", "barEnum2", results.get(1)); + assertQuickFix("xxxfoo", "xxxbar", results.get(2)); + assertQuickFix("xxxfooEnum2", "xxxbarEnum2", results.get(3)); + } + + @Test + public void testRenameEnumValueName() throws Exception { + EnumDataType enum1 = new EnumDataType("enum1", 4); + enum1.add("foo", 1); + enum1.add("xxx", 2); + + Enum dt = (Enum) addDataType(enum1); + setSearchTypes(enumValues); + + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Enum Value", item.getItemType()); + assertEquals(null, item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + assertEquals("bar", dt.getName(1)); + } + + @Test + public void testRenameEnumValueNameDuplicate() throws Exception { + EnumDataType enum1 = new EnumDataType("enum1", 4); + enum1.add("foo", 1); + enum1.add("bar", 2); + + Enum dt = (Enum) addDataType(enum1); + setSearchTypes(enumValues); + + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.WARNING, item.getStatus()); + assertEquals("New name not allowed because it duplicates an existing value name", + item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Enum Value", item.getItemType()); + assertEquals(null, item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.ERROR, item.getStatus()); + assertEquals("Rename enum value failed: bar already exists in this enum", + item.getStatusMessage()); + assertEquals("foo", item.getCurrent()); + assertEquals("foo", dt.getName(1)); + } + + @Test + public void testSearchEnumValueComments() throws Exception { + EnumDataType enum1 = new EnumDataType("enum1", 4); + enum1.add("ONE", 1, "ONE foo comment"); + enum1.add("TWO", 2, "TWO foo comment"); + + EnumDataType enum2 = new EnumDataType("enum2", 4); + enum1.add("THREE", 3, "THREE foo comment"); + enum1.add("FOUR", 4, "FOUR foo comment"); + + addDataType(enum1); + addDataType(enum2); + + setSearchTypes(dataTypeComments); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(4, results.size()); + sortByName(results); + + assertQuickFix("FOUR foo comment", "FOUR bar comment", results.get(0)); + assertQuickFix("ONE foo comment", "ONE bar comment", results.get(1)); + assertQuickFix("THREE foo comment", "THREE bar comment", results.get(2)); + assertQuickFix("TWO foo comment", "TWO bar comment", results.get(3)); + } + + @Test + public void testUpdateEnumValueComments() throws Exception { + EnumDataType enum1 = new EnumDataType("enum1", 4); + enum1.add("ONE", 1, "ONE foo comment"); + Enum dt = (Enum) addDataType(enum1); + + setSearchTypes(dataTypeComments); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Update", item.getActionName()); + assertEquals("Enum Comment", item.getItemType()); + assertEquals(null, item.getProgramLocation()); + assertEquals("ONE foo comment", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("ONE bar comment", item.getCurrent()); + assertEquals("ONE bar comment", dt.getComment("ONE")); + + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/ListingCommentsSearchAndReplaceTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/ListingCommentsSearchAndReplaceTest.java new file mode 100644 index 0000000000..14cd4a6082 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/ListingCommentsSearchAndReplaceTest.java @@ -0,0 +1,194 @@ +/* ### + * 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.replace; + +import static ghidra.program.model.listing.CommentType.*; +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.program.util.*; + +public class ListingCommentsSearchAndReplaceTest extends AbstractSearchAndReplaceTest { + + @Test + public void testSearchComments() throws Exception { + createComment(10, EOL, "EOLxxx abcxxxdef"); + createComment(20, PLATE, "PLATE xxx"); + createComment(30, PRE, "xxx PRE"); + createComment(30, POST, "POST abcxxxdef"); + createComment(30, REPEATABLE, "REPEATABLE xxx"); + + setSearchTypes(comments); + List results = query("xxx", "zzz", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(5, results.size()); + sortByAddress(results); + + assertQuickFix(10, "EOLxxx abcxxxdef", "EOLzzz abczzzdef", results.get(0)); + assertQuickFix(20, "PLATE xxx", "PLATE zzz", results.get(1)); + assertQuickFix(30, "xxx PRE", "zzz PRE", results.get(2)); + assertQuickFix(30, "POST abcxxxdef", "POST abczzzdef", results.get(3)); + assertQuickFix(30, "REPEATABLE xxx", "REPEATABLE zzz", results.get(4)); + } + + @Test + public void testChangingListingEolComments() throws Exception { + createComment(10, EOL, "EOL xxx abcxxxdef"); + + setSearchTypes(comments); + List results = query("xxx", "zzz", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Update", item.getActionName()); + assertEquals("Code Comment", item.getItemType()); + assertEquals(new EolCommentFieldLocation(program, addr(10), null, null, 0, 0, 0), + item.getProgramLocation()); + assertEquals("EOL xxx abcxxxdef", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("EOL zzz abczzzdef", item.getCurrent()); + } + + @Test + public void testChangingListingPostComments() throws Exception { + createComment(10, POST, "POST xxx abcxxxdef"); + + setSearchTypes(comments); + List results = query("xxx", "zzz", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Update", item.getActionName()); + assertEquals("Code Comment", item.getItemType()); + assertEquals(new PostCommentFieldLocation(program, addr(10), null, null, 0, 0), + item.getProgramLocation()); + assertEquals("POST xxx abcxxxdef", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("POST zzz abczzzdef", item.getCurrent()); + } + + @Test + public void testChangingListingPlateComments() throws Exception { + createComment(10, PLATE, "PLATE xxx abcxxxdef"); + + setSearchTypes(comments); + List results = query("xxx", "zzz", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Update", item.getActionName()); + assertEquals("Code Comment", item.getItemType()); + assertEquals(new PlateFieldLocation(program, addr(10), null, 0, 0, null, 0), + item.getProgramLocation()); + assertEquals("PLATE xxx abcxxxdef", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("PLATE zzz abczzzdef", item.getCurrent()); + } + + @Test + public void testChangingListingRepeatableComments() throws Exception { + createComment(10, REPEATABLE, "REPEATABLE xxx abcxxxdef"); + + setSearchTypes(comments); + List results = query("xxx", "zzz", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Update", item.getActionName()); + assertEquals("Code Comment", item.getItemType()); + assertEquals(new RepeatableCommentFieldLocation(program, addr(10), null, null, 0, 0, 0), + item.getProgramLocation()); + assertEquals("REPEATABLE xxx abcxxxdef", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("REPEATABLE zzz abczzzdef", item.getCurrent()); + } + + @Test + public void testChangingListingPreComments() throws Exception { + createComment(10, PRE, "PRE xxx abcxxxdef"); + + setSearchTypes(comments); + List results = query("xxx", "zzz", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Update", item.getActionName()); + assertEquals("Code Comment", item.getItemType()); + assertEquals(new CommentFieldLocation(program, addr(10), null, null, PRE.ordinal(), 0, 0), + item.getProgramLocation()); + assertEquals("PRE xxx abcxxxdef", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("PRE zzz abczzzdef", item.getCurrent()); + } + + @Test + public void testChangingListingCommentsWholeWordOn() throws Exception { + createComment(10, EOL, "EOL xxx abcxxxdef"); + + setSearchTypes(comments); + List results = query("xxx", "zzz", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Update", item.getActionName()); + assertEquals("Code Comment", item.getItemType()); + assertEquals(addr(10), item.getProgramLocation().getAddress()); + assertEquals("EOL xxx abcxxxdef", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("EOL zzz abcxxxdef", item.getCurrent()); + } +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/MemoryBlockSearchAndReplaceTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/MemoryBlockSearchAndReplaceTest.java new file mode 100644 index 0000000000..b02609b74f --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/MemoryBlockSearchAndReplaceTest.java @@ -0,0 +1,95 @@ +/* ### + * 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.replace; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.util.MemoryBlockStartFieldLocation; + +public class MemoryBlockSearchAndReplaceTest extends AbstractSearchAndReplaceTest { + @Test + public void testSearchMemoryBlocks() throws Exception { + + createBlock("foo", 0x10000); + createBlock("xxfooxx", 0x20000); + + setSearchTypes(memoryBlocks); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(2, results.size()); + sortByName(results); + + assertQuickFix(0x10000, "foo", "bar", results.get(0)); + assertQuickFix(0x20000, "xxfooxx", "xxbarxx", results.get(1)); + } + + @Test + public void testRenamingMemoryBlock() throws Exception { + MemoryBlock foo = createBlock("foo", 0x10000); + + setSearchTypes(memoryBlocks); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Memory Block", item.getItemType()); + assertEquals(new MemoryBlockStartFieldLocation(program, addr(0x10000), null, 0, 0, null, 0), + item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + assertEquals("bar", foo.getName()); + } + + @Test + public void testRenameMemoryBlockDuplicateOk() throws Exception { + MemoryBlock foo = createBlock("foo", 0x10000); + createBlock("bar", 0x20000); + + setSearchTypes(memoryBlocks); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Memory Block", item.getItemType()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + assertEquals("bar", foo.getName()); + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/ProgramTreeSearchAndReplaceTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/ProgramTreeSearchAndReplaceTest.java new file mode 100644 index 0000000000..1af7101377 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/ProgramTreeSearchAndReplaceTest.java @@ -0,0 +1,176 @@ +/* ### + * 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.replace; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.program.model.listing.*; +import ghidra.program.util.ProgramLocation; + +public class ProgramTreeSearchAndReplaceTest extends AbstractSearchAndReplaceTest { + + @Test + public void testSearchMemoryBlocks() throws Exception { + builder.createProgramTree("abc"); + builder.getOrCreateModule("abc", "foo"); + createFragment("abc", "foo", "xxfooxx", 10, 20); + + setSearchTypes(programTrees); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(2, results.size()); + sortByName(results); + + assertQuickFix("foo", "bar", results.get(0)); + assertQuickFix("xxfooxx", "xxbarxx", results.get(1)); + } + + @Test + public void testRenamingProgramTreeModule() throws Exception { + builder.createProgramTree("abc"); + ProgramModule module = builder.getOrCreateModule("abc", "foo"); + createFragment("abc", "foo", "frag1", 10, 20); + + setSearchTypes(programTrees); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Program Tree Module", item.getItemType()); + assertEquals(new ProgramLocation(program, addr(10)), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + assertEquals("bar", module.getName()); + } + + @Test + public void testRenamingProgramTreeFragment() throws Exception { + builder.createProgramTree("abc"); + builder.getOrCreateModule("abc", "xxx"); + ProgramFragment fragment = createFragment("abc", "xxx", "foo", 10, 20); + + setSearchTypes(programTrees); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Program Tree Fragment", item.getItemType()); + assertEquals(new ProgramLocation(program, addr(10)), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + assertEquals("bar", fragment.getName()); + } + + @Test + public void testRenameModuleDuplicate() throws Exception { + builder.createProgramTree("abc"); + ProgramModule module = builder.getOrCreateModule("abc", "foo"); + createFragment("abc", "foo", "frag1", 10, 20); + builder.getOrCreateModule("abc", "bar"); + createFragment("abc", "bar", "frag2", 30, 40); + + setSearchTypes(programTrees); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.WARNING, item.getStatus()); + assertEquals("The name \"bar\" already exists in module \"TestX86\"", + item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Program Tree Module", item.getItemType()); + assertEquals(new ProgramLocation(program, addr(10)), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.ERROR, item.getStatus()); + assertEquals("Rename Failed! bar already exists", item.getStatusMessage()); + assertEquals("foo", item.getCurrent()); + assertEquals("foo", module.getName()); + } + + @Test + public void testRenameFragmentDuplicate() throws Exception { + builder.createProgramTree("abc"); + builder.getOrCreateModule("abc", "module1"); + ProgramFragment fragment = createFragment("abc", "module1", "foo", 10, 20); + createFragment("abc", "module1", "bar", 30, 40); + + setSearchTypes(programTrees); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.WARNING, item.getStatus()); + assertEquals("The name \"bar\" already exists in module \"module1\"", + item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Program Tree Fragment", item.getItemType()); + assertEquals(new ProgramLocation(program, addr(10)), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.ERROR, item.getStatus()); + assertEquals("Rename Failed! bar already exists", item.getStatusMessage()); + assertEquals("foo", item.getCurrent()); + assertEquals("foo", fragment.getName()); + } + + private ProgramFragment createFragment(String treeName, String moduleName, String fragmentName, + int start, int end) throws Exception { + String startAddress = Long.toHexString(start); + String endAddress = Long.toHexString(end); + builder.createFragment(treeName, moduleName, fragmentName, startAddress, endAddress); + Group[] children = program.getListing().getRootModule(treeName).getChildren(); + for (Group group : children) { + if (group.getName().equals(moduleName) && group instanceof ProgramModule module) { + Group[] grandChildren = module.getChildren(); + for (Group child : grandChildren) { + if (child.getName().equals(fragmentName) && + child instanceof ProgramFragment fragment) { + return fragment; + } + } + } + } + return null; + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/SearchAndReplaceDialogTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/SearchAndReplaceDialogTest.java new file mode 100644 index 0000000000..8a596a6f89 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/SearchAndReplaceDialogTest.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.replace; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.*; + +import docking.ActionContext; +import docking.action.DockingActionIf; +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +import ghidra.features.base.quickfix.*; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.database.ProgramDB; +import ghidra.program.database.symbol.SymbolManager; +import ghidra.program.model.address.Address; +import ghidra.program.model.symbol.Symbol; +import ghidra.test.*; + +public class SearchAndReplaceDialogTest extends AbstractGhidraHeadedIntegrationTest { + private TestEnv env; + private PluginTool tool; + private ProgramDB program; + private DockingActionIf searchAndReplaceAction; + private SearchAndReplaceDialog dialog; + + @Before + public void setUp() throws Exception { + env = new TestEnv(); + tool = env.getTool(); + tool.addPlugin(SearchAndReplacePlugin.class.getName()); + tool.addPlugin(CodeBrowserPlugin.class.getName()); + + SearchAndReplacePlugin plugin = getPlugin(tool, SearchAndReplacePlugin.class); + + ToyProgramBuilder builder = new ToyProgramBuilder("Test", true); + builder.createLabel("0x100", "myFooLabel"); + builder.createLabel("0x200", "myBarLabel"); + program = builder.getProgram(); + env.open(program); + env.showTool(); + searchAndReplaceAction = getAction(plugin, "Search And Replace"); + ActionContext actionContext = tool.getActiveComponentProvider().getActionContext(null); + performAction(searchAndReplaceAction, actionContext, false); + dialog = waitForDialogComponent(SearchAndReplaceDialog.class); + assertNotNull(dialog); + } + + @After + public void tearDown() throws Exception { + env.dispose(); + } + + @Test + public void testBasicEnablementAndStatus() { + assertEquals("Please enter search text", getStatusText()); + assertFalse(isOkEnabled()); + + enterText("search text", "replace text"); + + assertFalse(isOkEnabled()); + assertEquals("Please select at least one \"search for\" item to search!", getStatusText()); + + selectSearchType("Labels"); + + assertEquals("", getStatusText()); + assertTrue(isOkEnabled()); + } + + @Test + public void testInvalidRegex() { + enterText("(abc", ""); // "(abc" is valid for normal search, invalid for regex + selectSearchType("Labels"); + assertEquals("", getStatusText()); + assertTrue(isOkEnabled()); + + selectRegEx(true); + assertEquals("", getStatusText()); + assertFalse(isOkEnabled()); + + selectRegEx(false); + assertEquals("", getStatusText()); + assertTrue(isOkEnabled()); + } + + @Test + public void testResultsProviderAppearsWithResults() { + SearchAndReplaceProvider provider = executeBasicQuery(); + List data = provider.getTableModel().getModelData(); + assertEquals(2, data.size()); + assertEquals(QuickFixStatus.NONE, data.get(0).getStatus()); + assertEquals(QuickFixStatus.NONE, data.get(1).getStatus()); + } + + @Test + public void testApplyResults() { + SearchAndReplaceProvider provider = executeBasicQuery(); + List data = provider.getTableModel().getModelData(); + assertEquals(2, data.size()); + executeAllItems(provider); + assertEquals(QuickFixStatus.DONE, data.get(0).getStatus()); + assertEquals(QuickFixStatus.DONE, data.get(1).getStatus()); + assertEquals("yourFooLabel", getSymbol(0x100).getName()); + assertEquals("yourBarLabel", getSymbol(0x200).getName()); + } + + @Test + public void testApplyResultsToJustSelectedItem() { + SearchAndReplaceProvider provider = executeBasicQuery(); + List data = provider.getTableModel().getModelData(); + assertEquals(2, data.size()); + selectItem(provider, 0); + executeSelectedItems(provider); + assertEquals(QuickFixStatus.DONE, data.get(0).getStatus()); + assertEquals(QuickFixStatus.NONE, data.get(1).getStatus()); + assertEquals("myFooLabel", getSymbol(0x100).getName()); + assertEquals("yourBarLabel", getSymbol(0x200).getName()); + assertEquals(1, getSelectedRow(provider)); + } + + private int getSelectedRow(SearchAndReplaceProvider provider) { + return runSwing(() -> provider.getSelectedRow()); + } + + private void selectItem(SearchAndReplaceProvider provider, int index) { + runSwing(() -> provider.setSelection(index, index)); + } + + private Symbol getSymbol(long offset) { + SymbolManager symbolTable = program.getSymbolTable(); + Symbol primarySymbol = symbolTable.getPrimarySymbol(addr(offset)); + return primarySymbol; + } + + private void executeAllItems(SearchAndReplaceProvider provider) { + runSwing(() -> provider.executeAll()); + waitForTasks(); + } + + private void executeSelectedItems(SearchAndReplaceProvider provider) { + runSwing(() -> provider.applySelected()); + waitForTasks(); + } + + private Address addr(long offset) { + return program.getAddressFactory().getDefaultAddressSpace().getAddress(offset); + } + + private SearchAndReplaceProvider executeBasicQuery() { + enterText("my", "your"); + selectSearchType("Labels"); + pressOk(); + SearchAndReplaceProvider provider = + waitForComponentProvider(SearchAndReplaceProvider.class); + assertNotNull(provider); + QuickFixTableModel tableModel = provider.getTableModel(); + waitForTableModel(tableModel); + return provider; + } + + private void pressOk() { + runSwing(() -> dialog.okCallback()); + } + + private void selectRegEx(boolean b) { + runSwing(() -> dialog.selectRegEx(b)); + } + + private boolean isOkEnabled() { + return runSwing(() -> dialog.isOkEnabled()); + } + + private void selectSearchType(String searchType) { + runSwing(() -> dialog.selectSearchType(searchType)); + } + + private void enterText(String searchText, String replaceText) { + runSwing(() -> dialog.setSarchAndReplaceText(searchText, replaceText)); + } + + private String getStatusText() { + return runSwing(() -> dialog.getStatusText().trim()); + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/SymbolsSearchAndReplaceTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/SymbolsSearchAndReplaceTest.java new file mode 100644 index 0000000000..951a97b343 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/replace/SymbolsSearchAndReplaceTest.java @@ -0,0 +1,553 @@ +/* ### + * 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.replace; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import ghidra.features.base.quickfix.QuickFix; +import ghidra.features.base.quickfix.QuickFixStatus; +import ghidra.program.model.data.ByteDataType; +import ghidra.program.model.data.DataType; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.Namespace; +import ghidra.program.model.symbol.Symbol; +import ghidra.program.util.LabelFieldLocation; +import ghidra.util.exception.CancelledException; + +public class SymbolsSearchAndReplaceTest extends AbstractSearchAndReplaceTest { + + @Test + public void testLabelsSearchNotCaseSensitive() throws CancelledException { + createLabel(10, "foo"); + createLabel(20, "fooxxxx"); + createLabel(30, "xxxFoox"); + createLabel(40, "xxxxfOO"); + createLabel(50, "xxxfoxo"); + + setSearchTypes(labels); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(4, results.size()); + sortByAddress(results); + + assertQuickFix(10, "foo", "bar", results.get(0)); + assertQuickFix(20, "fooxxxx", "barxxxx", results.get(1)); + assertQuickFix(30, "xxxFoox", "xxxbarx", results.get(2)); + assertQuickFix(40, "xxxxfOO", "xxxxbar", results.get(3)); + } + + @Test + public void testLabelsSearchWholeWord() throws CancelledException { + createLabel(10, "foo"); + createLabel(20, "fooxxxx"); + createLabel(30, "xxxFoox"); + createLabel(40, "xxxxfOO"); + createLabel(50, "xxxfoxo"); + + setSearchTypes(labels); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + + assertEquals(1, results.size()); + + assertQuickFix(10, "foo", "bar", results.get(0)); + } + + @Test + public void testLabelsSearchCaseSensitiveSearch() throws CancelledException { + createLabel(10, "fooxxxx"); + createLabel(20, "Fooxxxx"); + createLabel(30, "xxFOOxxx"); + createLabel(40, "foo"); + createLabel(50, "xxxfoo"); + + setSearchTypes(labels); + List results = query("Foo", "bar", CASE_SENSITIVE_ON, WHOLE_WORD_OFF); + + assertEquals(1, results.size()); + assertQuickFix(20, "Fooxxxx", "barxxxx", results.get(0)); + } + + @Test + public void testLabelsSearchRegEx() throws CancelledException { + createLabel(10, "fooxxxx"); + createLabel(20, "Fooxxxx"); + createLabel(30, "xxFOOxxx"); + createLabel(40, "foo"); + createLabel(50, "xxxfoo"); + + setSearchTypes(labels); + List results = queryRegEx("^Foo$", "bar", CASE_SENSITIVE_OFF); + + assertEquals(1, results.size()); + assertQuickFix(40, "foo", "bar", results.get(0)); + + } + + @Test + public void testLabelsSearchRegExCaptureGroups() throws CancelledException { + createLabel(10, "fooxxxx"); + createLabel(20, "Fooxxxx"); + createLabel(30, "xxFOOxxx"); + createLabel(40, "foo"); + createLabel(50, "xxBARxxx"); + + setSearchTypes(labels); + List results = queryRegEx("xx(.*)xxx", "zz$1zzz", CASE_SENSITIVE_OFF); + sortByAddress(results); + assertEquals(2, results.size()); + assertQuickFix(30, "xxFOOxxx", "zzFOOzzz", results.get(0)); + assertQuickFix(50, "xxBARxxx", "zzBARzzz", results.get(1)); + } + + @Test + public void testRenamingLabel() throws CancelledException { + Symbol s = createLabel(10, "foo"); + createLabel(20, "fooxxxx"); + createLabel(30, "xxxFoox"); + createLabel(40, "xxxxfOO"); + createLabel(50, "xxxfoxo"); + + setSearchTypes(labels); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Label", item.getItemType()); + assertEquals("Global", item.getPath()); + assertEquals(new LabelFieldLocation(s), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + } + + @Test + public void testRenameLabelDuplicate() throws CancelledException { + Symbol s = createLabel(10, "foo"); + createLabel(20, "bar"); + + setSearchTypes(labels); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.WARNING, item.getStatus()); + assertEquals("There is already a symbol named \"bar\" in namespace \"Global\"", + item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Label", item.getItemType()); + assertEquals("Global", item.getPath()); + assertEquals(new LabelFieldLocation(s), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + } + + @Test + public void testSearchFunctions() throws Exception { + createFunction(10, "foo"); + createFunction(20, "fooxxxx"); + createFunction(30, "xxxFoox"); + createFunction(40, "xxxxfOO"); + createFunction(50, "xxxfoxo"); + + setSearchTypes(functions); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(4, results.size()); + sortByAddress(results); + + assertQuickFix(10, "foo", "bar", results.get(0)); + assertQuickFix(20, "fooxxxx", "barxxxx", results.get(1)); + assertQuickFix(30, "xxxFoox", "xxxbarx", results.get(2)); + assertQuickFix(40, "xxxxfOO", "xxxxbar", results.get(3)); + } + + @Test + public void testRenamingFunction() throws Exception { + Function function = createFunction(10, "foo"); + + setSearchTypes(functions); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Function", item.getItemType()); + assertEquals("Global", item.getPath()); + assertEquals(function.getSymbol().getProgramLocation(), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + + assertEquals("bar", function.getName()); + + } + + @Test + public void testRenameFunctionDuplicate() throws Exception { + Function function = createFunction(10, "foo"); + createFunction(20, "bar"); + + setSearchTypes(functions); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.WARNING, item.getStatus()); + assertEquals("There is already a symbol named \"bar\" in namespace \"Global\"", + item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Function", item.getItemType()); + assertEquals("Global", item.getPath()); + assertEquals(function.getSymbol().getProgramLocation(), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + assertEquals("bar", function.getName()); + } + + @Test + public void testSearchNamespaces() throws Exception { + Namespace global = program.getGlobalNamespace(); + Namespace aaa = createNamespace(global, "aaa"); + + createNamespace(global, "foo"); + createNamespace(global, "fooxxxx"); + createNamespace(aaa, "xxxFoox"); + createNamespace(aaa, "xxxxfOO"); + createNamespace(global, "xxxfoxo"); + + setSearchTypes(namespaces); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(4, results.size()); + sortByName(results); + + assertQuickFix("foo", "bar", results.get(0)); + assertQuickFix("fooxxxx", "barxxxx", results.get(1)); + assertQuickFix("xxxFoox", "xxxbarx", results.get(2)); + assertQuickFix("xxxxfOO", "xxxxbar", results.get(3)); + } + + @Test + public void testRenamingNamespace() throws Exception { + Namespace global = program.getGlobalNamespace(); + Namespace foo = createNamespace(global, "foo"); + + setSearchTypes(namespaces); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Namespace", item.getItemType()); + assertEquals("Global", item.getPath()); + assertEquals(foo.getSymbol().getProgramLocation(), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + assertEquals("bar", foo.getName()); + } + + @Test + public void testRenameNamespaceDuplicate() throws Exception { + Namespace global = program.getGlobalNamespace(); + Namespace foo = createNamespace(global, "foo"); + createNamespace(global, "bar"); + + setSearchTypes(namespaces); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.WARNING, item.getStatus()); + assertEquals("There is already a symbol named \"bar\" in namespace \"Global\"", + item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Namespace", item.getItemType()); + assertEquals("Global", item.getPath()); + assertEquals(foo.getSymbol().getProgramLocation(), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.ERROR, item.getStatus()); + assertEquals( + "Rename Failed! A Namespace symbol with name bar already exists in namespace Global", + item.getStatusMessage()); + assertEquals("foo", item.getCurrent()); + assertEquals("foo", foo.getName()); + } + + @Test + public void testSearchClasses() throws Exception { + Namespace global = program.getGlobalNamespace(); + Namespace aaa = createNamespace(global, "aaa"); + + createClass(global, "foo"); + createClass(global, "fooxxxx"); + createClass(aaa, "xxxFoox"); + createClass(aaa, "xxxxfOO"); + createClass(global, "xxxfoxo"); + + setSearchTypes(classes); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(4, results.size()); + sortByName(results); + + assertQuickFix("foo", "bar", results.get(0)); + assertQuickFix("fooxxxx", "barxxxx", results.get(1)); + assertQuickFix("xxxFoox", "xxxbarx", results.get(2)); + assertQuickFix("xxxxfOO", "xxxxbar", results.get(3)); + } + + @Test + public void testRenamingClass() throws Exception { + Namespace global = program.getGlobalNamespace(); + GhidraClass foo = createClass(global, "foo"); + + setSearchTypes(classes); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Class", item.getItemType()); + assertEquals("Global", item.getPath()); + assertEquals(foo.getSymbol().getProgramLocation(), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + assertEquals("bar", foo.getName()); + } + + @Test + public void testRenameClassWithDuplicate() throws Exception { + Namespace global = program.getGlobalNamespace(); + Namespace foo = createClass(global, "foo"); + createClass(global, "bar"); + + setSearchTypes(classes); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.WARNING, item.getStatus()); + assertEquals("There is already a symbol named \"bar\" in namespace \"Global\"", + item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Class", item.getItemType()); + assertEquals("Global", item.getPath()); + assertEquals(foo.getSymbol().getProgramLocation(), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.ERROR, item.getStatus()); + assertEquals( + "Rename Failed! A Class symbol with name bar already exists in namespace Global", + item.getStatusMessage()); + assertEquals("foo", item.getCurrent()); + assertEquals("foo", foo.getName()); + } + + @Test + public void testSearchParameters() throws Exception { + createFunction(10, "aaa", "foo", "xxxfooxxx"); + createFunction(20, "bbb", "foo"); + + setSearchTypes(parameters); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(3, results.size()); + sortByAddress(results); + + assertQuickFix("foo", "bar", results.get(0)); + assertQuickFix("xxxfooxxx", "xxxbarxxx", results.get(1)); + assertQuickFix("foo", "bar", results.get(2)); + } + + @Test + public void testRenamingParameter() throws Exception { + Function function = createFunction(10, "aaa", "xxxfooxxx"); + Parameter parameter = function.getParameter(1); + + setSearchTypes(parameters); + List results = query("xxxfooxxx", "xxxbarxxx", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Parameter", item.getItemType()); + assertEquals("aaa", item.getPath()); + assertEquals(parameter.getSymbol().getProgramLocation(), item.getProgramLocation()); + assertEquals("xxxfooxxx", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("xxxbarxxx", item.getCurrent()); + + assertEquals("xxxbarxxx", parameter.getName()); + + } + + @Test + public void testRenameParameterDuplicate() throws Exception { + Function function = createFunction(10, "aaa", "foo", "bar"); + Parameter parameter = function.getParameter(1); + + setSearchTypes(parameters); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.WARNING, item.getStatus()); + assertEquals("There is already a symbol named \"bar\" in namespace \"aaa\"", + item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Parameter", item.getItemType()); + assertEquals("aaa", item.getPath()); + assertEquals(parameter.getSymbol().getProgramLocation(), item.getProgramLocation()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.ERROR, item.getStatus()); + assertEquals( + "Rename Failed! A Parameter symbol with name bar already exists in namespace aaa", + item.getStatusMessage()); + assertEquals("foo", item.getCurrent()); + assertEquals("foo", parameter.getName()); + } + + @Test + public void testSearchLocalVars() throws Exception { + Function f1 = createFunction(10, "aaa"); + Function f2 = createFunction(20, "bbb"); + DataType dt = new ByteDataType(); + builder.createLocalVariable(f1, "foo", dt, 0); + builder.createLocalVariable(f1, "xxxfooxxx", dt, 4); + builder.createLocalVariable(f2, "foo", dt, 0); + + setSearchTypes(localVariables); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_OFF); + + assertEquals(3, results.size()); + + assertQuickFix("foo", "bar", results.get(0)); + assertQuickFix("xxxfooxxx", "xxxbarxxx", results.get(1)); + assertQuickFix("foo", "bar", results.get(2)); + } + + @Test + public void testRenamingLocalVariable() throws Exception { + Function f1 = createFunction(10, "aaa"); + Function f2 = createFunction(20, "bbb"); + DataType dt = new ByteDataType(); + builder.createLocalVariable(f1, "foo", dt, 0); + builder.createLocalVariable(f2, "bar", dt, 4); + + setSearchTypes(localVariables); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.NONE, item.getStatus()); + assertEquals("Not Applied", item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Local Var", item.getItemType()); + assertEquals("aaa", item.getPath()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.DONE, item.getStatus()); + assertEquals("Applied", item.getStatusMessage()); + assertEquals("bar", item.getCurrent()); + } + + @Test + public void testRenameVariableDuplicate() throws Exception { + Function f1 = createFunction(10, "aaa"); + DataType dt = new ByteDataType(); + builder.createLocalVariable(f1, "foo", dt, 0); + builder.createLocalVariable(f1, "bar", dt, 4); + + setSearchTypes(localVariables); + List results = query("foo", "bar", CASE_SENSITIVE_OFF, WHOLE_WORD_ON); + assertEquals(1, results.size()); + QuickFix item = results.get(0); + + assertEquals(QuickFixStatus.WARNING, item.getStatus()); + assertEquals("There is already a symbol named \"bar\" in namespace \"aaa\"", + item.getStatusMessage()); + assertEquals("Rename", item.getActionName()); + assertEquals("Local Var", item.getItemType()); + assertEquals("aaa", item.getPath()); + assertEquals("foo", item.getCurrent()); + + performAction(item); + + assertEquals(QuickFixStatus.ERROR, item.getStatus()); + assertEquals( + "Rename Failed! A Local Var symbol with name bar already exists in namespace aaa", + item.getStatusMessage()); + assertEquals("foo", item.getCurrent()); + } + +} diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/comments/DecompilerCommentsActionFactory.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/comments/DecompilerCommentsActionFactory.java index b6a0f80089..341655df3e 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/comments/DecompilerCommentsActionFactory.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/comments/DecompilerCommentsActionFactory.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. @@ -83,7 +83,7 @@ public class DecompilerCommentsActionFactory extends CommentsActionFactory { if (!isCommentSupported(loc)) { return false; } - return CommentType.isCommentAllowed(getCodeUnit(actionContext), loc); + return CommentTypeUtils.isCommentAllowed(getCodeUnit(actionContext), loc); } @Override @@ -139,7 +139,7 @@ public class DecompilerCommentsActionFactory extends CommentsActionFactory { return CodeUnit.PRE_COMMENT; } CodeUnit cu = getCodeUnit(context); - return CommentType.getCommentType(cu, getLocationForContext(context), CodeUnit.NO_COMMENT); + return CommentTypeUtils.getCommentType(cu, getLocationForContext(context), CodeUnit.NO_COMMENT); } } } diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskMonitorComponent.java b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskMonitorComponent.java index acc4b9b28b..7dec9d15eb 100644 --- a/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskMonitorComponent.java +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskMonitorComponent.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. diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/AccumulatorSizeException.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/AccumulatorSizeException.java new file mode 100644 index 0000000000..eb4d2ac79b --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/AccumulatorSizeException.java @@ -0,0 +1,30 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.datastruct; + +public class AccumulatorSizeException extends RuntimeException { + + private int maxSize; + + public AccumulatorSizeException(int maxSize) { + super("Maximum capacity exceeded: " + maxSize); + this.maxSize = maxSize; + } + + public int getMaxSize() { + return maxSize; + } +} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/SizeRestrictedAccumulatorWrapper.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/SizeRestrictedAccumulatorWrapper.java new file mode 100644 index 0000000000..3f82b69eaf --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/datastruct/SizeRestrictedAccumulatorWrapper.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.util.datastruct; + +import java.util.*; + +public class SizeRestrictedAccumulatorWrapper implements Accumulator { + + private Accumulator accumulator; + private int maxSize; + + /** + * Constructor. + * + * @param accumulator the accumulator to pass items to + * @param maxSize the maximum number of items this accumulator will hold + */ + public SizeRestrictedAccumulatorWrapper(Accumulator accumulator, int maxSize) { + this.accumulator = Objects.requireNonNull(accumulator); + this.maxSize = maxSize; + } + + @Override + public Iterator iterator() { + return accumulator.iterator(); + } + + @Override + public void add(T t) { + if (accumulator.size() >= maxSize) { + throw new AccumulatorSizeException(maxSize); + } + accumulator.add(t); + } + + @Override + public void addAll(Collection collection) { + for (T t : collection) { + accumulator.add(t); + } + } + + @Override + public boolean contains(T t) { + return accumulator.contains(t); + } + + @Override + public Collection get() { + return accumulator.get(); + } + + @Override + public int size() { + return accumulator.size(); + } + +} diff --git a/Ghidra/Framework/Gui/certification.manifest b/Ghidra/Framework/Gui/certification.manifest index db69886e7e..4a5d0cc298 100644 --- a/Ghidra/Framework/Gui/certification.manifest +++ b/Ghidra/Framework/Gui/certification.manifest @@ -64,6 +64,7 @@ src/main/resources/images/sortascending.png||GHIDRA||||END| src/main/resources/images/sortdescending.png||GHIDRA||||END| src/main/resources/images/stack.png||GHIDRA||||END| src/main/resources/images/text_align_justify.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| +src/main/resources/images/tick.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| src/main/resources/images/up.png||GHIDRA||||END| src/main/resources/images/video-x-generic16.png||Tango Icons - Public Domain|||tango icon set|END| src/main/resources/images/viewmagfit.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END| diff --git a/Ghidra/Framework/Gui/data/gui.theme.properties b/Ghidra/Framework/Gui/data/gui.theme.properties index 47965ebae8..ddb4fca6f7 100644 --- a/Ghidra/Framework/Gui/data/gui.theme.properties +++ b/Ghidra/Framework/Gui/data/gui.theme.properties @@ -66,7 +66,7 @@ font.monospaced = monospaced-plain-12 // Icons files icon.flag = flag.png icon.lock = kgpg.png -icon.checkmark.green = checkmark_green.gif +icon.checkmark.green = tick.png icon.empty = EmptyIcon16.gif icon.empty.20 = EmptyIcon.gif diff --git a/Ghidra/Framework/Gui/src/main/resources/images/tick.png b/Ghidra/Framework/Gui/src/main/resources/images/tick.png new file mode 100644 index 0000000000000000000000000000000000000000..a9925a06ab02db30c1e7ead9c701c15bc63145cb GIT binary patch literal 537 zcmV+!0_OdRP)Hs{AQG2a)rMyf zFQK~pm1x3+7!nu%-M`k}``c>^00{o_1pjWJUTfl8mg=3qGEl8H@}^@w`VUx0_$uy4 z2FhRqKX}xI*?Tv1DJd8z#F#0c%*~rM30HE1@2o5m~}ZyoWhqv>ql{V z1ZGE0lgcoK^lx+eqc*rAX1Ky;Xx3U%u#zG!m-;eD1Qsn@kf3|F9qz~|95=&g3(7!X zB}JAT>RU;a%vaNOGnJ%e1=K6eAh43c(QN8RQ6~GP%O}Jju$~Ld*%`mO1pnull clears the comment * @@ -161,9 +200,21 @@ public interface CodeUnit extends MemBuffer, PropertySet { * * @throws IllegalArgumentException if type is not one of the * three types of comments supported + * @deprecated use {@link #setComment(CommentType, String)} instead */ + @Deprecated public void setComment(int commentType, String comment); + /** + * Set the comment for the given comment type. Passing null clears the comment + * + * @param type of comment to set + * @param comment comment for code unit; null clears the comment + */ + public default void setComment(CommentType type, String comment) { + setComment(type.ordinal(), comment); + } + /** * Set the comment (with each line in its own string) for the given comment type * @@ -175,6 +226,10 @@ public interface CodeUnit extends MemBuffer, PropertySet { */ public void setCommentAsArray(int commentType, String[] comment); + public default void setCommentAsArray(CommentType type, String[] comment) { + setCommentAsArray(type.ordinal(), comment); + } + /** * Get length of this code unit. * NOTE: If an {@link Instruction#isLengthOverridden() instruction length-override} is diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/CodeUnitComments.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/CodeUnitComments.java new file mode 100644 index 0000000000..b38d7df047 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/CodeUnitComments.java @@ -0,0 +1,40 @@ +/* ### + * 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.listing; + +/** + * Container for all the comments at an address + */ +public class CodeUnitComments { + private String[] comments; + + public CodeUnitComments(String[] comments) { + if (comments.length != CommentType.values().length) { + throw new IllegalArgumentException("comment array size does not match enum size!"); + } + this.comments = comments; + } + + /** + * Get the comment for the given comment type + * @param type the {@link CommentType} to retrieve + * @return the comment of the given type or null if no comment of that type exists + */ + public String getComment(CommentType type) { + return comments[type.ordinal()]; + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/CommentType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/CommentType.java new file mode 100644 index 0000000000..befb6a7a21 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/CommentType.java @@ -0,0 +1,28 @@ +/* ### + * 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.listing; + +/** + * Types of comments that be placed at an address or on a {@link CodeUnit} + */ +public enum CommentType { + EOL, // comments that appear at the end of the line + PRE, // comments that appear before the code unit + POST, // comments that appear after the code unit + PLATE, // comments that appear before the code unit with a decorated border + REPEATABLE // comments that appear at locations that refer to the address + // where this comment is defined +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Group.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Group.java index 102352ef34..bb4c40a0f3 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Group.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Group.java @@ -15,6 +15,11 @@ */ package ghidra.program.model.listing; +import java.util.ArrayList; +import java.util.List; + +import ghidra.program.model.address.Address; +import ghidra.program.util.GroupPath; import ghidra.util.exception.DuplicateNameException; /** @@ -34,14 +39,14 @@ public interface Group { * * @param comment the comment. */ - public void setComment(String comment); + public void setComment(String comment); /** * Obtains the name that has been associated with this fragment. A fragment will * always have a name and it will be unique within the set of all fragment and * module names. */ - public String getName(); + public String getName(); /** * Sets the name of this fragment. @@ -52,8 +57,8 @@ public interface Group { * thrown if the name being set is already in use by another fragment or a * module. */ - public void setName(String name) throws DuplicateNameException; - + public void setName(String name) throws DuplicateNameException; + /** * Returns whether this fragment contains the given code unit. * @@ -61,7 +66,7 @@ public interface Group { * * @return true if the code unit is in the fragment, false otherwise. */ - public boolean contains(CodeUnit codeUnit); + public boolean contains(CodeUnit codeUnit); /** * Obtains the number of parent's of this fragment. If a fragment is in a module @@ -71,21 +76,55 @@ public interface Group { * * @return the number of parents of this fragment. */ - public int getNumParents(); + public int getNumParents(); /** * Returns a list of the modules which are parents for this group. */ - public ProgramModule[] getParents(); + public ProgramModule[] getParents(); /** * Returns the names of the modules which are parents to this * fragment. */ - public String[] getParentNames(); - + public String[] getParentNames(); + /** * Returns the name of the tree that this group belongs to. */ - public String getTreeName(); + public String getTreeName(); + + /** + * Returns true if this group has been deleted from the program + * @return true if this group has been deleted from the program + */ + public boolean isDeleted(); + + public Address getMinAddress(); + + public Address getMaxAddress(); + + /** + * Returns one of many possible GroupPaths for this group. Since Fragments can belong in + * more than one module, there can be multiple legitimate group paths for a group. This method + * arbitrarily returns one valid group path. + * @return one of several possible group paths for this group + */ + public default GroupPath getGroupPath() { + List parentNames = getParentNames(this); + return new GroupPath(parentNames.toArray(new String[parentNames.size()])); + } + + private static List getParentNames(Group group) { + Group[] parents = group.getParents(); + if (parents == null || parents.length == 0) { + List list = new ArrayList<>(); + list.add(group.getName()); + return list; + } + Group parent = parents[0]; + List names = getParentNames(parent); + names.add(group.getName()); + return names; + } } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Listing.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Listing.java index e299391049..3a383175e6 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Listing.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Listing.java @@ -144,19 +144,51 @@ public interface Listing { */ public CodeUnitIterator getCommentCodeUnitIterator(int commentType, AddressSetView addrSet); + /** + * Get a forward code unit iterator over code units that have the specified + * comment type. + * + * @param type the comment type + * @param addrSet address set to iterate code unit comments over + * @return a CodeUnitIterator that returns all code units from the indicated + * address set that have the specified comment type defined + */ + public default CodeUnitIterator getCommentCodeUnitIterator(CommentType type, + AddressSetView addrSet) { + return getCommentCodeUnitIterator(type.ordinal(), addrSet); + } + /** * Get a forward iterator over addresses that have the specified comment * type. * * @param commentType type defined in CodeUnit - * @param addrSet address set + * @param addrSet address set to iterate code unit comments over + * @param forward true to iterator from lowest address to highest, false + * highest to lowest + * @return an AddressIterator that returns all addresses from the indicated + * address set that have the specified comment type defined + * @deprecated use {@link #getCommentAddressIterator(CommentType, AddressSetView, boolean)} + */ + @Deprecated + public AddressIterator getCommentAddressIterator(int commentType, AddressSetView addrSet, + boolean forward); + + /** + * Get a forward iterator over addresses that have the specified comment + * type. + * + * @param type the type of comment to iterate over + * @param addrSet address set to iterate code unit comments over * @param forward true to iterator from lowest address to highest, false * highest to lowest * @return an AddressIterator that returns all addresses from the indicated * address set that have the specified comment type defined */ - public AddressIterator getCommentAddressIterator(int commentType, AddressSetView addrSet, - boolean forward); + public default AddressIterator getCommentAddressIterator(CommentType type, + AddressSetView addrSet, boolean forward) { + return getCommentAddressIterator(type.ordinal(), addrSet, forward); + } /** * Get a forward iterator over addresses that have any type of comment. @@ -179,9 +211,30 @@ public interface Listing { * of that type exists for this code unit * @throws IllegalArgumentException if type is not one of the types of * comments supported + * @deprecated use {@link #getComment(CommentType, Address)} */ + @Deprecated public String getComment(int commentType, Address address); + /** + * Get all the comments at the given address. + * @param address the address get comments + * @return a CodeUnitComments object that has all the comments at the address. + */ + public CodeUnitComments getAllComments(Address address); + + /** + * Get the comment for the given type at the specified address. + * + * @param type the comment type to retrieve + * @param address the address of the comment. + * @return the comment string of the appropriate type or null if no comment + * of that type exists for this code unit + */ + public default String getComment(CommentType type, Address address) { + return getComment(type.ordinal(), address); + } + /** * Set the comment for the given comment type at the specified address. * @@ -191,9 +244,24 @@ public interface Listing { * @param comment comment to set at the address * @throws IllegalArgumentException if type is not one of the types of * comments supported + * @deprecated use {@link #setComment(Address, CommentType, String)} */ + @Deprecated public void setComment(Address address, int commentType, String comment); + /** + * Set the comment for the given comment type at the specified address. + * + * @param address the address of the comment. + * @param type the type of comment to set + * @param comment comment to set at the address + * @throws IllegalArgumentException if type is not one of the types of + * comments supported + */ + public default void setComment(Address address, CommentType type, String comment) { + setComment(address, type.ordinal(), comment); + } + /** * get a CodeUnit iterator that will iterate over the entire address space. * @@ -983,4 +1051,10 @@ public interface Listing { */ public CommentHistory[] getCommentHistory(Address addr, int commentType); + /** + * Returns the number of addresses where at least one comment type has been applied. + * @return the number of addresses where at least one comment type has been applied + */ + public long getCommentAddressCount(); + } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/StubListing.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/StubListing.java index eb1a5846ee..20cc01e52c 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/StubListing.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/StubListing.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. @@ -90,11 +90,21 @@ public class StubListing implements Listing { throw new UnsupportedOperationException(); } + @Override + public long getCommentAddressCount() { + throw new UnsupportedOperationException(); + } + @Override public String getComment(int commentType, Address address) { throw new UnsupportedOperationException(); } + @Override + public CodeUnitComments getAllComments(Address address) { + throw new UnsupportedOperationException(); + } + @Override public void setComment(Address address, int commentType, String comment) { throw new UnsupportedOperationException(); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/CommentType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/CommentTypeUtils.java similarity index 97% rename from Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/CommentType.java rename to Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/CommentTypeUtils.java index da4ab56d3b..eaba1dbc17 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/CommentType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/CommentTypeUtils.java @@ -1,13 +1,12 @@ /* ### * 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. @@ -18,7 +17,7 @@ package ghidra.program.util; import ghidra.program.model.listing.CodeUnit; -public class CommentType { +public class CommentTypeUtils { /** * Get the comment type from the current location. If the cursor diff --git a/Ghidra/Framework/Utility/src/main/java/ghidra/util/UserSearchUtils.java b/Ghidra/Framework/Utility/src/main/java/ghidra/util/UserSearchUtils.java index 892421dc15..5b9e81b62d 100644 --- a/Ghidra/Framework/Utility/src/main/java/ghidra/util/UserSearchUtils.java +++ b/Ghidra/Framework/Utility/src/main/java/ghidra/util/UserSearchUtils.java @@ -299,9 +299,15 @@ public class UserSearchUtils { } /** - * Escapes regex characters, optionally turning globbing characters into valid regex syntax. + * Convert user entered text into a regular expression, escaping regex characters, + * optionally turning globbing characters into valid regex syntax. + * @param input the user entered text to be converted to a regular expression. + * @param allowGlobbing if true, '*' and '?' will be converted to equivalent regular expression + * syntax for wildcard matching, otherwise they will be treated as literal characters to be + * part of the search text. + * @return a converted text string suitable for use in a regular expression. */ - private static String convertUserInputToRegex(String input, boolean allowGlobbing) { + public static String convertUserInputToRegex(String input, boolean allowGlobbing) { String escaped = input; if (allowGlobbing) { diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/SearchAndReplaceScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/SearchAndReplaceScreenShots.java new file mode 100644 index 0000000000..2c77c6ae84 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/SearchAndReplaceScreenShots.java @@ -0,0 +1,92 @@ +/* ### + * 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 help.screenshot; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +import ghidra.features.base.replace.*; + +/** + * Screenshots for help/topics/Search/Search_Memory.htm + */ +public class SearchAndReplaceScreenShots extends AbstractSearchScreenShots { + + private CodeBrowserPlugin cb; + private SearchAndReplacePlugin plugin; + + @Override + @Before + public void setUp() throws Exception { + + super.setUp(); + + plugin = env.getPlugin(SearchAndReplacePlugin.class); + cb = env.getPlugin(CodeBrowserPlugin.class); + + env.showTool(); + } + + @Test + public void testSearchAndReplaceDialog() { + performAction("Search And Replace", "SearchAndReplacePlugin", false); + waitForSwing(); + + SearchAndReplaceDialog dialog = + (SearchAndReplaceDialog) getDialog(SearchAndReplaceDialog.class); + + runSwing(() -> { + dialog.setSarchAndReplaceText("value", "amount"); + dialog.selectSearchType("Labels"); + dialog.selectSearchType("Functions"); + dialog.selectSearchType("Comments"); + dialog.selectSearchType("Datatypes"); + dialog.selectSearchType("Datatype Fields"); + dialog.selectSearchType("Datatype Comments"); + dialog.selectSearchType("Parameters"); + }); + + captureDialog(dialog); + + } + + @Test + public void testSearchAndReplaceResults() { + performAction("Search And Replace", "SearchAndReplacePlugin", false); + waitForSwing(); + + SearchAndReplaceDialog dialog = + (SearchAndReplaceDialog) getDialog(SearchAndReplaceDialog.class); + + runSwing(() -> { + dialog.setSarchAndReplaceText("value", "amount"); + dialog.selectSearchType("Labels"); + dialog.selectSearchType("Functions"); + dialog.selectSearchType("Comments"); + dialog.selectSearchType("Datatypes"); + dialog.selectSearchType("Datatype Fields"); + dialog.selectSearchType("Datatype Comments"); + dialog.selectSearchType("Parameters"); + }); + pressOkOnDialog(); + + SearchAndReplaceProvider provider = + waitForComponentProvider(SearchAndReplaceProvider.class); + + captureIsolatedProvider(provider, 700, 500); + } +}