diff --git a/Ghidra/Features/Base/ghidra_scripts/AddSourceFileScript.java b/Ghidra/Features/Base/ghidra_scripts/AddSourceFileScript.java new file mode 100644 index 0000000000..cfdcd2d687 --- /dev/null +++ b/Ghidra/Features/Base/ghidra_scripts/AddSourceFileScript.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. + */ +// Adds a SourceFile with a user-defined path and name to the program. +//@category SourceMapping +import java.util.HexFormat; + +import org.apache.commons.lang3.StringUtils; + +import ghidra.app.script.GhidraScript; +import ghidra.features.base.values.GhidraValuesMap; +import ghidra.program.database.sourcemap.SourceFile; +import ghidra.program.database.sourcemap.SourceFileIdType; +import ghidra.util.MessageType; + +public class AddSourceFileScript extends GhidraScript { + + private static final String PATH = "Source File Path"; + private static final String ID_TYPE = "Id Type"; + private static final String IDENTIFIER = "Identifier"; + + @Override + protected void run() throws Exception { + if (isRunningHeadless()) { + println("This script must be run through the Ghidra GUI"); + return; + } + if (currentProgram == null) { + popup("This script requires an open program"); + return; + } + if (!currentProgram.hasExclusiveAccess()) { + popup("This script requires exclusive access to the program"); + return; + } + + GhidraValuesMap values = new GhidraValuesMap(); + values.defineString(PATH, "/"); + SourceFileIdType[] idTypes = SourceFileIdType.values(); + String[] enumNames = new String[idTypes.length]; + for (int i = 0; i < enumNames.length; ++i) { + enumNames[i] = idTypes[i].name(); + } + values.defineChoice(ID_TYPE, SourceFileIdType.NONE.name(), enumNames); + values.defineString(IDENTIFIER, StringUtils.EMPTY); + + values.setValidator((valueMap, status) -> { + String path = valueMap.getString(PATH); + SourceFileIdType idType = SourceFileIdType.valueOf(values.getChoice(ID_TYPE)); + byte[] identifier = null; + if (idType != SourceFileIdType.NONE) { + identifier = HexFormat.of().parseHex(values.getString(IDENTIFIER)); + } + try { + SourceFile srcFile = new SourceFile(path, idType, identifier); + if (currentProgram.getSourceFileManager().containsSourceFile(srcFile)) { + status.setStatusText("SourceFile " + srcFile + " already exists", + MessageType.ERROR); + return false; + } + } + catch (IllegalArgumentException e) { + status.setStatusText(e.getMessage(), MessageType.ERROR); + return false; + } + return true; + }); + askValues("Enter (Absolute) Source File URI Path", + "e.g.: /usr/bin/echo, /C:/Programs/file.exe", values); + String absolutePath = values.getString(PATH); + SourceFileIdType idType = SourceFileIdType.valueOf(values.getChoice(ID_TYPE)); + byte[] identifier = null; + if (idType != SourceFileIdType.NONE) { + identifier = HexFormat.of().parseHex(values.getString(IDENTIFIER)); + } + SourceFile srcFile = new SourceFile(absolutePath, idType, identifier); + currentProgram.getSourceFileManager().addSourceFile(srcFile); + printf("Successfully added source file %s%n", srcFile.toString()); + } + +} diff --git a/Ghidra/Features/Base/ghidra_scripts/AddSourceMapEntryScript.java b/Ghidra/Features/Base/ghidra_scripts/AddSourceMapEntryScript.java new file mode 100644 index 0000000000..de72e8707e --- /dev/null +++ b/Ghidra/Features/Base/ghidra_scripts/AddSourceMapEntryScript.java @@ -0,0 +1,146 @@ +/* ### + * 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. + */ +// Add a source map entry for the current selection. +// The current selection must consist of a single address range. +// If there is no selection, a length 0 entry will be added at the current address. +//@category SourceMapping +import java.util.*; + +import ghidra.app.script.GhidraScript; +import ghidra.features.base.values.GhidraValuesMap; +import ghidra.program.database.sourcemap.SourceFile; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.sourcemap.*; +import ghidra.util.MessageType; + +public class AddSourceMapEntryScript extends GhidraScript { + + private static final String LINE_NUM = "Line Number"; + private static final String SOURCE_FILE = "Source File"; + + @Override + protected void run() throws Exception { + if (isRunningHeadless()) { + println("This script must be run through the Ghidra GUI"); + return; + } + if (currentProgram == null) { + popup("This script requires an open program"); + return; + } + if (!currentProgram.hasExclusiveAccess()) { + popup("This script requires exclusive access to the program"); + return; + } + + SourceFileManager sourceManager = currentProgram.getSourceFileManager(); + List sourceFiles = sourceManager.getAllSourceFiles(); + if (sourceFiles.isEmpty()) { + popup("You must first add at least one source file to the program"); + return; + } + + boolean valid = isCurrentSelectionValid(); + if (!valid) { + return; // checkCurrentSelection will tell the user what the problem is + } + + Address baseAddr = + currentSelection == null ? currentAddress : currentSelection.getMinAddress(); + long length = currentSelection == null ? 0 : currentSelection.getNumAddresses(); + + AddressRange currentRange = + currentSelection == null ? null : currentSelection.getAddressRanges().next(); + + Map stringsToSourceFiles = new HashMap<>(); + sourceFiles.forEach(sf -> stringsToSourceFiles.put(sf.toString(), sf)); + + GhidraValuesMap values = new GhidraValuesMap(); + values.defineInt(LINE_NUM, 1); + String[] sourceFileArray = stringsToSourceFiles.keySet().toArray(new String[0]); + Arrays.sort(sourceFileArray); + values.defineChoice(SOURCE_FILE, sourceFileArray[0], sourceFileArray); + + values.setValidator((valueMap, status) -> { + int lineNum = values.getInt(LINE_NUM); + if (lineNum < 0) { + status.setStatusText("Line number cannot be negative", MessageType.ERROR); + return false; + } + if (currentRange == null) { + return true; // length 0 entry, nothing else to check + } + SourceFile source = new SourceFile(values.getChoice(SOURCE_FILE)); + + for (SourceMapEntry entry : sourceManager.getSourceMapEntries(source, lineNum)) { + // because checkCurrentSelection returned true, if the entries intersect + // they must be equal + if (entry.getLength() != 0 && entry.getRange().intersects(currentRange)) { + status.setStatusText(currentSelection + " is already mapped to " + + source.getPath() + ":" + lineNum); + return false; + } + } + return true; + }); + + askValues("Source Map Info", "Enter Source Map Info (length = " + length + ")", values); + int lineNum = values.getInt(LINE_NUM); + String fileString = values.getChoice(SOURCE_FILE); + SourceFile source = stringsToSourceFiles.get(fileString); + sourceManager.addSourceMapEntry(source, lineNum, baseAddr, length); + } + + // check that the selected range doesn't already intersect a SourceMapEntry with a + // conflicting range. + private boolean isCurrentSelectionValid() { + if (currentSelection == null) { + return true; + } + if (currentSelection.getNumAddressRanges() != 1) { + popup("This script requires the current selection to be a single address range"); + return false; + } + AddressRange range = currentSelection.getFirstRange(); + Address end = range.getMaxAddress(); + SourceMapEntryIterator iter = + currentProgram.getSourceFileManager().getSourceMapEntryIterator(end, false); + while (iter.hasNext()) { + SourceMapEntry entry = iter.next(); + if (!entry.getBaseAddress().getAddressSpace().equals(range.getAddressSpace())) { + return true; // iterator has exhausted entries in the address space, no conflicts + // are possible + } + if (entry.getLength() == 0) { + continue; // length 0 entries can't conflict + } + AddressRange entryRange = entry.getRange(); + if (entryRange.equals(range)) { + return true; // range is the same as a range already in the db, so no problems + } + if (entryRange.intersects(range)) { + popup("Selection conflicts with existing entry " + entry.toString()); + return false; + } + if (entryRange.getMaxAddress().compareTo(range.getMinAddress()) < 0) { + return true; // no conflicting entries exists + } + } + return true; + } + +} diff --git a/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoScript.java b/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoCommentScript.java similarity index 93% rename from Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoScript.java rename to Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoCommentScript.java index aeee1140d7..540c10f730 100644 --- a/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoCommentScript.java @@ -4,16 +4,18 @@ * 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. */ -// Adds DWARF source file line number info to the current binary +// Adds DWARF source file line number info to the current binary as EOL comments. +// Note that you can run this script on a program that has already been analyzed by the +// DWARF analyzer. //@category DWARF import java.io.IOException; import java.util.List; @@ -29,7 +31,7 @@ import ghidra.program.model.listing.CodeUnit; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; -public class DWARFLineInfoScript extends GhidraScript { +public class DWARFLineInfoCommentScript extends GhidraScript { @Override protected void run() throws Exception { DWARFSectionProvider dsp = diff --git a/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoSourceMapScript.java b/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoSourceMapScript.java new file mode 100644 index 0000000000..92d3cd901d --- /dev/null +++ b/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoSourceMapScript.java @@ -0,0 +1,154 @@ +/* ### + * 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. + */ +// Adds DWARF source file line number info to the current program as source map entries. +// Note that you can run this script on a program that has already been analyzed by the +// DWARF analyzer. +//@category DWARF +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import ghidra.app.script.GhidraScript; +import ghidra.app.util.bin.BinaryReader; +import ghidra.app.util.bin.format.dwarf.*; +import ghidra.app.util.bin.format.dwarf.line.DWARFLine.SourceFileAddr; +import ghidra.app.util.bin.format.dwarf.sectionprovider.DWARFSectionProvider; +import ghidra.app.util.bin.format.dwarf.sectionprovider.DWARFSectionProviderFactory; +import ghidra.framework.store.LockException; +import ghidra.program.database.sourcemap.SourceFile; +import ghidra.program.database.sourcemap.SourceFileIdType; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressOverflowException; +import ghidra.program.model.sourcemap.SourceFileManager; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; + +public class DWARFLineInfoSourceMapScript extends GhidraScript { + public static final int ENTRY_MAX_LENGTH = 1000; + + @Override + protected void run() throws Exception { + + if (!currentProgram.hasExclusiveAccess()) { + Msg.showError(this, null, "Exclusive Access Required", + "Must have exclusive access to a program to add source map info"); + return; + } + DWARFSectionProvider dsp = + DWARFSectionProviderFactory.createSectionProviderFor(currentProgram, monitor); + if (dsp == null) { + printerr("Unable to find DWARF information"); + return; + } + + DWARFImportOptions importOptions = new DWARFImportOptions(); + try (DWARFProgram dprog = new DWARFProgram(currentProgram, importOptions, monitor, dsp)) { + dprog.init(monitor); + addSourceLineInfo(dprog); + } + } + + private void addSourceLineInfo(DWARFProgram dprog) + throws CancelledException, IOException, LockException, AddressOverflowException { + BinaryReader reader = dprog.getDebugLineBR(); + if (reader == null) { + popup("Unable to get reader for debug line info"); + return; + } + int entryCount = 0; + monitor.initialize(reader.length(), "DWARF Source Map Info"); + List compUnits = dprog.getCompilationUnits(); + SourceFileManager sourceManager = currentProgram.getSourceFileManager(); + List sourceInfo = new ArrayList<>(); + for (DWARFCompilationUnit cu : compUnits) { + sourceInfo.addAll(cu.getLine().getAllSourceFileAddrInfo(cu, reader)); + } + sourceInfo.sort((i, j) -> Long.compareUnsigned(i.address(), j.address())); + monitor.initialize(sourceInfo.size()); + for (int i = 0; i < sourceInfo.size(); i++) { + monitor.checkCancelled(); + monitor.increment(1); + SourceFileAddr sourceFileAddr = sourceInfo.get(i); + if (sourceFileAddr.isEndSequence()) { + continue; + } + Address addr = dprog.getCodeAddress(sourceFileAddr.address()); + if (!currentProgram.getMemory().getExecuteSet().contains(addr)) { + printerr( + "entry for non-executable address; skipping: file %s line %d address: %s %x" + .formatted(sourceFileAddr.fileName(), sourceFileAddr.lineNum(), + addr.toString(), sourceFileAddr.address())); + continue; + } + + long length = getLength(i, sourceInfo); + if (length < 0) { + println( + "Error computing entry length for file %s line %d address %s %x; replacing" + + " with length 0 entry".formatted(sourceFileAddr.fileName(), + sourceFileAddr.lineNum(), addr.toString(), sourceFileAddr.address())); + length = 0; + } + if (length > ENTRY_MAX_LENGTH) { + println( + ("entry for file %s line %d address: %s %x length %d too large, replacing " + + "with length 0 entry").formatted(sourceFileAddr.fileName(), + sourceFileAddr.lineNum(), addr.toString(), sourceFileAddr.address(), + length)); + length = 0; + } + if (sourceFileAddr.fileName() == null) { + continue; + } + SourceFile source = null; + try { + SourceFileIdType type = + sourceFileAddr.md5() == null ? SourceFileIdType.NONE : SourceFileIdType.MD5; + source = new SourceFile(sourceFileAddr.fileName(), type, sourceFileAddr.md5()); + sourceManager.addSourceFile(source); + } + catch (IllegalArgumentException e) { + printerr("Exception creating source file %s".formatted(e.getMessage())); + continue; + } + try { + sourceManager.addSourceMapEntry(source, sourceFileAddr.lineNum(), addr, length); + } + catch (IllegalArgumentException e) { + printerr(e.getMessage()); + continue; + } + entryCount++; + } + println("Added " + entryCount + " source map entries"); + } + + private long getLength(int i, List allSFA) { + SourceFileAddr iAddr = allSFA.get(i); + long iOffset = iAddr.address(); + for (int j = i + 1; j < allSFA.size(); j++) { + SourceFileAddr current = allSFA.get(j); + long currentAddr = current.address(); + if (current.isEndSequence()) { + return currentAddr + 1 - iOffset; + } + if (currentAddr != iOffset) { + return currentAddr - iOffset; + } + } + return -1; + } +} diff --git a/Ghidra/Features/Base/ghidra_scripts/RemoveSourceMapEntryScript.java b/Ghidra/Features/Base/ghidra_scripts/RemoveSourceMapEntryScript.java new file mode 100644 index 0000000000..00a2c15935 --- /dev/null +++ b/Ghidra/Features/Base/ghidra_scripts/RemoveSourceMapEntryScript.java @@ -0,0 +1,63 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Select and remove a source map entry at the current address. +//@category SourceMapping +import java.util.*; + +import ghidra.app.script.GhidraScript; +import ghidra.features.base.values.GhidraValuesMap; +import ghidra.program.model.sourcemap.SourceMapEntry; + +public class RemoveSourceMapEntryScript extends GhidraScript { + + @Override + protected void run() throws Exception { + if (isRunningHeadless()) { + println("This script must be run through the Ghidra GUI"); + return; + } + if (currentProgram == null) { + popup("This script requires an open program"); + return; + } + if (!currentProgram.hasExclusiveAccess()) { + popup("Modifying the source map requires exclusive access to the program"); + return; + } + List entries = + currentProgram.getSourceFileManager().getSourceMapEntries(currentAddress); + if (entries.isEmpty()) { + popup("No source map entries at " + currentAddress); + return; + } + + Map stringToEntry = new HashMap<>(); + List choices = new ArrayList<>(); + for (SourceMapEntry entry : entries) { + String entryString = entry.toString(); + stringToEntry.put(entryString, entry); + choices.add(entryString); + } + GhidraValuesMap values = new GhidraValuesMap(); + String[] choiceArray = choices.toArray(new String[] {}); + values.defineChoice("Entry", choiceArray[0], choiceArray); + askValues("Select SourceMapEntry to remove", null, values); + String selectedString = values.getChoice("Entry"); + SourceMapEntry toDelete = stringToEntry.get(selectedString); + currentProgram.getSourceFileManager().removeSourceMapEntry(toDelete); + } + +} diff --git a/Ghidra/Features/Base/ghidra_scripts/SelectAddressesMappedToSourceFileScript.java b/Ghidra/Features/Base/ghidra_scripts/SelectAddressesMappedToSourceFileScript.java new file mode 100644 index 0000000000..c555f557b8 --- /dev/null +++ b/Ghidra/Features/Base/ghidra_scripts/SelectAddressesMappedToSourceFileScript.java @@ -0,0 +1,113 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Sets the current selection based on source file and line number parameters +//@category SourceMapping +import java.util.*; + +import ghidra.app.script.GhidraScript; +import ghidra.features.base.values.GhidraValuesMap; +import ghidra.program.database.sourcemap.SourceFile; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.sourcemap.SourceFileManager; +import ghidra.program.model.sourcemap.SourceMapEntry; +import ghidra.util.MessageType; +import ghidra.util.SourceFileUtils; +import ghidra.util.SourceFileUtils.SourceLineBounds; + +public class SelectAddressesMappedToSourceFileScript extends GhidraScript { + + private static final String SOURCE_FILE = "Source File"; + private static final String MIN_LINE = "Minimum Source Line"; + private static final String MAX_LINE = " Maximum Source Line"; + + @Override + protected void run() throws Exception { + if (isRunningHeadless()) { + println("This script must be run through the Ghidra GUI"); + return; + } + if (currentProgram == null) { + popup("This script requires an open program"); + return; + } + + SourceFileManager sourceManager = currentProgram.getSourceFileManager(); + + List sourceFiles = sourceManager.getMappedSourceFiles(); + if (sourceFiles.isEmpty()) { + popup(currentProgram.getName() + " contains no mapped source files"); + return; + } + + GhidraValuesMap sourceFileValue = new GhidraValuesMap(); + + Map stringsToSourceFiles = new HashMap<>(); + sourceFiles.forEach(sf -> stringsToSourceFiles.put(sf.toString(), sf)); + String[] sourceFileArray = stringsToSourceFiles.keySet().toArray(new String[0]); + Arrays.sort(sourceFileArray); + + sourceFileValue.defineChoice(SOURCE_FILE, sourceFileArray[0], sourceFileArray); + askValues("Select Source File", "Source File", sourceFileValue); + String sfString = sourceFileValue.getChoice(SOURCE_FILE); + + SourceFile mappedSourceFile = stringsToSourceFiles.get(sfString); + SourceLineBounds bounds = + SourceFileUtils.getSourceLineBounds(currentProgram, mappedSourceFile); + GhidraValuesMap boundValues = new GhidraValuesMap(); + boundValues.defineInt(MIN_LINE, bounds.min()); + boundValues.setInt(MIN_LINE, bounds.min()); + boundValues.defineInt(MAX_LINE, bounds.max()); + boundValues.setInt(MAX_LINE, bounds.max()); + + boundValues.setValidator((valueMap, status) -> { + int minLine = boundValues.getInt(MIN_LINE); + if (minLine < 0) { + status.setStatusText("Line number cannot be negative", MessageType.ERROR); + return false; + } + int maxLine = boundValues.getInt(MAX_LINE); + if (maxLine < minLine) { + status.setStatusText("Max line cannot be less than min line", MessageType.ERROR); + return false; + } + return true; + }); + + setReusePreviousChoices(false); + askValues("Select Line Bounds", "Bounds for " + mappedSourceFile.getFilename(), + boundValues); + setReusePreviousChoices(true); + + int minLine = boundValues.getInt(MIN_LINE); + int maxLine = boundValues.getInt(MAX_LINE); + AddressSet selection = new AddressSet(); + List entries = + sourceManager.getSourceMapEntries(mappedSourceFile, minLine, maxLine); + for (SourceMapEntry entry : entries) { + if (entry.getLength() == 0) { + selection.add(entry.getBaseAddress()); + continue; + } + selection.add(entry.getRange()); + } + if (selection.isEmpty()) { + popup("No addresses for the selected file and range"); + return; + } + setCurrentSelection(selection); + } + +} diff --git a/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/CodeBrowserOptions.htm b/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/CodeBrowserOptions.htm index f37777b799..d620ba7539 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/CodeBrowserOptions.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/CodeBrowserPlugin/CodeBrowserOptions.htm @@ -993,7 +993,7 @@ Option to display the raw PCode directly in the Code Browser (i.e., detailed varnode specifications are provided).

-

Maximum Lines to Display - The maximum number of lines used to display PCode. Any +

Maximum Lines to Display - The maximum number of lines used to display PCode. Any additional lines of PCode will not be shown.

@@ -1009,6 +1009,24 @@

Selection Color - Set the Browser Selection color.

+ +

Source Map Field

+
+

Show Filename Only - + If selected, only the file name will be shown in the Listing field (rather than the full + path).

+ +

Show Source Info at Every Address - If selected, source map information will be + displayed for each address in the Listing. Otherwise the source information for a given + source map entry will only be displayed at the minimum address of the corresponding + range.

+ +

Maximum Number of Source Map Entries to Display - Maximum number of source + map entries to display per address.

+ +

Show Identifier - If selected, the source file identifier (md5, sha1,...) will + be shown in the Listing. Note that a source file might not have an identifier.

+

Template Display Options

diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImportOptions.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImportOptions.java index 215256cceb..0a0bbd0a46 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImportOptions.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImportOptions.java @@ -38,10 +38,10 @@ public class DWARFImportOptions { "Include source code location info (filename:linenumber) in comments attached to the " + "Ghidra datatype or function or variable created."; - private static final String OPTION_SOURCE_LINEINFO = "Output Source Line Info"; + private static final String OPTION_SOURCE_LINEINFO = "Import Source Line Info"; private static final String OPTION_SOURCE_LINEINFO_DESC = - "Place end-of-line comments containg the source code filename and line number at " + - "each location provided in the DWARF data"; + "Create source map entries containing the source code filename, line number, address, and" + + " length at each location provided in the DWARF data"; private static final String OPTION_OUTPUT_DWARF_DIE_INFO = "Output DWARF DIE Info"; private static final String OPTION_OUTPUT_DWARF_DIE_INFO_DESC = @@ -78,6 +78,10 @@ public class DWARFImportOptions { private static final String OPTION_DEFAULT_CC_DESC = "Name of default calling convention to assign to functions (e.g. __cdecl, __stdcall, etc), or leave blank."; + private static final String OPTION_MAX_SOURCE_ENTRY_LENGTH = "Maximum Source Map Entry Length"; + private static final String OPTION_MAX_SOURCE_ENTRY_LENGTH_DESC = + "Maximum length for a source map entry. Longer lengths will be replaced with 0"; + //================================================================================================== // Old Option Names - Should stick around for multiple major versions after 10.2 //================================================================================================== @@ -111,9 +115,10 @@ public class DWARFImportOptions { private boolean specialCaseSizedBaseTypes = true; private boolean importLocalVariables = true; private boolean useBookmarks = true; - private boolean outputSourceLineInfo = false; + private boolean outputSourceLineInfo = true; private boolean ignoreParamStorage = false; private String defaultCC = ""; + private long maxSourceMapEntryLength = 2000; /** * Create new instance @@ -376,10 +381,20 @@ public class DWARFImportOptions { return useBookmarks; } + /** + * Option to control whether source map info from DWARF is stored in the Program. + * + * @return {@code true} if option turned on + */ public boolean isOutputSourceLineInfo() { return outputSourceLineInfo; } + /** + * Option to control whether source map info from DWARF is stored in the Program. + * + * @param outputSourceLineInfo true to turn option on, false to turn off + */ public void setOutputSourceLineInfo(boolean outputSourceLineInfo) { this.outputSourceLineInfo = outputSourceLineInfo; } @@ -400,6 +415,29 @@ public class DWARFImportOptions { this.defaultCC = defaultCC; } + /** + * Option to control the maximum length of a source map entry. If a longer length is calculated + * it will be replaced with 0. + * + * @return max source map entry length + */ + public long getMaxSourceMapEntryLength() { + return maxSourceMapEntryLength; + } + + /** + * Option to control the maximum length of a source map entry. If a longer length is calculated + * it will be replaced with 0. + * + * @param maxLength new max source entry length + */ + public void setMaxSourceMapEntryLength(long maxLength) { + if (maxLength < 0) { + maxLength = 0; + } + maxSourceMapEntryLength = maxLength; + } + /** * See {@link Analyzer#registerOptions(Options, ghidra.program.model.listing.Program)} * @@ -440,6 +478,8 @@ public class DWARFImportOptions { OPTION_IGNORE_PARAM_STORAGE_DESC); options.registerOption(OPTION_DEFAULT_CC, getDefaultCC(), null, OPTION_DEFAULT_CC_DESC); + options.registerOption(OPTION_MAX_SOURCE_ENTRY_LENGTH, maxSourceMapEntryLength, null, + OPTION_MAX_SOURCE_ENTRY_LENGTH_DESC); } /** @@ -467,6 +507,8 @@ public class DWARFImportOptions { setIgnoreParamStorage( options.getBoolean(OPTION_IGNORE_PARAM_STORAGE, isIgnoreParamStorage())); setDefaultCC(options.getString(OPTION_DEFAULT_CC, getDefaultCC())); + setMaxSourceMapEntryLength( + options.getLong(OPTION_MAX_SOURCE_ENTRY_LENGTH, getMaxSourceMapEntryLength())); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImporter.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImporter.java index cc20a69628..87ce51f13b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImporter.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImporter.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,19 +16,22 @@ package ghidra.app.util.bin.format.dwarf; import java.io.IOException; -import java.util.Collections; -import java.util.List; +import java.util.*; import ghidra.app.plugin.core.datamgr.util.DataTypeUtils; import ghidra.app.util.bin.BinaryReader; import ghidra.app.util.bin.format.dwarf.line.DWARFLine.SourceFileAddr; +import ghidra.app.util.bin.format.dwarf.line.DWARFLineProgramExecutor; +import ghidra.framework.store.LockException; +import ghidra.program.database.sourcemap.SourceFile; import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressOverflowException; import ghidra.program.model.data.*; -import ghidra.program.model.listing.CodeUnit; +import ghidra.program.model.listing.*; +import ghidra.program.model.sourcemap.SourceFileManager; import ghidra.util.Msg; import ghidra.util.Swing; -import ghidra.util.exception.CancelledException; -import ghidra.util.exception.DuplicateNameException; +import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; import utility.function.Dummy; @@ -81,8 +84,8 @@ public class DWARFImporter { monitor.checkCancelled(); monitor.incrementProgress(1); - if ( (monitor.getProgress() % 5) == 0 ) { - /* balance between getting work done and pampering the swing thread */ + if ((monitor.getProgress() % 5) == 0) { + /* balance between getting work done and pampering the swing thread */ Swing.runNow(Dummy.runnable()); } @@ -174,28 +177,124 @@ public class DWARFImporter { return new CategoryPath(newRoot, cpParts.subList(origRootParts.size(), cpParts.size())); } - private void addSourceLineInfo(BinaryReader reader) throws CancelledException, IOException { - if ( reader == null ) { + private void addSourceLineInfo(BinaryReader reader) + throws CancelledException, IOException, LockException { + if (reader == null) { + Msg.warn(this, "Can't add source line info - reader is null"); return; } - monitor.initialize(reader.length(), "DWARF Source Line Info"); + int entryCount = 0; + Program ghidraProgram = prog.getGhidraProgram(); + BookmarkManager bookmarkManager = ghidraProgram.getBookmarkManager(); + long maxLength = prog.getImportOptions().getMaxSourceMapEntryLength(); + boolean errorBookmarks = prog.getImportOptions().isUseBookmarks(); List compUnits = prog.getCompilationUnits(); + monitor.initialize(compUnits.size(), "Reading DWARF Source Map Info"); + SourceFileManager sourceManager = ghidraProgram.getSourceFileManager(); + List sourceInfo = new ArrayList<>(); for (DWARFCompilationUnit cu : compUnits) { - try { - monitor.checkCancelled(); - monitor.setProgress(cu.getLine().getStartOffset()); - List allSFA = cu.getLine().getAllSourceFileAddrInfo(cu, reader); - for (SourceFileAddr sfa : allSFA) { - Address addr = prog.getCodeAddress(sfa.address()); - DWARFUtil.appendComment(prog.getGhidraProgram(), addr, CodeUnit.EOL_COMMENT, "", - "%s:%d".formatted(sfa.fileName(), sfa.lineNum()), ";"); - } + monitor.increment(); + sourceInfo.addAll(cu.getLine().getAllSourceFileAddrInfo(cu, reader)); + } + sourceInfo.sort((i, j) -> Long.compareUnsigned(i.address(), j.address())); + monitor.initialize(sourceInfo.size(), "Applying DWARF Source Map Info"); + for (int i = 0; i < sourceInfo.size() - 1; i++) { + monitor.checkCancelled(); + monitor.increment(1); + SourceFileAddr sfa = sourceInfo.get(i); + if (sfa.isEndSequence()) { + continue; } - catch (IOException e) { - Msg.error(this, - "Failed to read DWARF line info for cu %d".formatted(cu.getUnitNumber()), e); + Address addr = prog.getCodeAddress(sfa.address()); + if (!ghidraProgram.getMemory().getExecuteSet().contains(addr)) { + String errorString = + "entry for non-executable address; skipping: file %s line %d address: %s %x" + .formatted(sfa.fileName(), sfa.lineNum(), addr.toString(), + sfa.address()); + + reportError(bookmarkManager, errorString, addr, errorBookmarks); + continue; + } + + long length = getLength(i, sourceInfo); + if (length < 0) { + length = 0; + String errorString = + "Error calculating entry length for file %s line %d address %s %x; replacing " + + "with length 0 entry".formatted(sfa.fileName(), sfa.lineNum(), + addr.toString(), sfa.address()); + reportError(bookmarkManager, errorString, addr, errorBookmarks); + } + if (length > maxLength) { + String errorString = ("entry for file %s line %d address: %s %x length %d too" + + " large, replacing with length 0 entry").formatted(sfa.fileName(), + sfa.lineNum(), addr.toString(), sfa.address(), length); + length = 0; + reportError(bookmarkManager, errorString, addr, errorBookmarks); + } + + SourceFile source = null; + try { + source = new SourceFile(sfa.fileName()); + sourceManager.addSourceFile(source); + } + catch (IllegalArgumentException e) { + Msg.error(this, "Exception creating source file: %s".formatted(e.getMessage())); + continue; + } + try { + sourceManager.addSourceMapEntry(source, sfa.lineNum(), addr, length); + } + catch (AddressOverflowException e) { + String errorString = "AddressOverflowException for source map entry %s %d %s %x %d" + .formatted(source.getFilename(), sfa.lineNum(), addr.toString(), + sfa.address(), length); + reportError(bookmarkManager, errorString, addr, errorBookmarks); + continue; + } + entryCount++; + } + Msg.info(this, "Added %d source map entries".formatted(entryCount)); + } + + private void reportError(BookmarkManager bManager, String errorString, Address addr, + boolean errorBookmarks) { + if (errorBookmarks) { + bManager.setBookmark(addr, BookmarkType.ERROR, DWARFProgram.DWARF_BOOKMARK_CAT, + errorString); + } + else { + Msg.error(this, errorString); + } + } + + /** + * In the DWARF format, source line info is only recorded for an address x + * if the info for x differs from the info for address x-1. + * To calculate the length of a source map entry, we need to look for the next + * SourceFileAddr with a different address (there can be multiple records per address + * so there's no guarantee that the SourceFileAddr at position i+1 has a different + * address). Special end-of-sequence markers are used to mark the end of a function, + * so if we find one of these we stop searching. These markers have their addresses tweaked + * by one, which we undo (see {@link DWARFLineProgramExecutor#executeExtended}). + * @param i starting index + * @param allSFA sorted list of SourceFileAddr + * @return computed length or -1 on error + */ + private long getLength(int i, List allSFA) { + SourceFileAddr iAddr = allSFA.get(i); + long iOffset = iAddr.address(); + for (int j = i + 1; j < allSFA.size(); j++) { + SourceFileAddr current = allSFA.get(j); + long currentAddr = current.address(); + if (current.isEndSequence()) { + return currentAddr + 1 - iOffset; + } + if (currentAddr != iOffset) { + return currentAddr - iOffset; } } + return -1; } /** @@ -205,7 +304,8 @@ public class DWARFImporter { * @throws DWARFException * @throws CancelledException */ - public DWARFImportSummary performImport() throws IOException, DWARFException, CancelledException { + public DWARFImportSummary performImport() + throws IOException, DWARFException, CancelledException { monitor.setIndeterminate(false); monitor.setShowProgressValue(true); @@ -231,7 +331,18 @@ public class DWARFImporter { } if (importOptions.isOutputSourceLineInfo()) { - addSourceLineInfo(prog.getDebugLineBR()); + if (!prog.getGhidraProgram().hasExclusiveAccess()) { + Msg.showError(this, null, "Unable to add source map info", + "Exclusive access to the program is required to add source map info"); + } + else { + try { + addSourceLineInfo(prog.getDebugLineBR()); + } + catch (LockException e) { + throw new AssertException("LockException after exclusive access verified"); + } + } } importSummary.totalElapsedMS = System.currentTimeMillis() - start_ts; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFProgram.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFProgram.java index d8304b878d..de3f7a4db8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFProgram.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFProgram.java @@ -54,7 +54,7 @@ public class DWARFProgram implements Closeable { public static final CategoryPath DWARF_ROOT_CATPATH = CategoryPath.ROOT.extend(DWARF_ROOT_NAME); public static final CategoryPath UNCAT_CATPATH = DWARF_ROOT_CATPATH.extend("_UNCATEGORIZED_"); - private static final String DWARF_BOOKMARK_CAT = "DWARF"; + public static final String DWARF_BOOKMARK_CAT = "DWARF"; private static final int NAME_HASH_REPLACEMENT_SIZE = 8 + 2 + 2; private static final String ELLIPSES_STR = "..."; protected static final EnumSet REF_ATTRS = diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/line/DWARFLine.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/line/DWARFLine.java index a0b8eb45fb..d98aefd5ec 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/line/DWARFLine.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/line/DWARFLine.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. @@ -270,14 +270,20 @@ public class DWARFLine { return lpe; } - public record SourceFileAddr(long address, String fileName, int lineNum) {} + public record SourceFileAddr(long address, String fileName, byte[] md5, int lineNum, + boolean isEndSequence) {} public List getAllSourceFileAddrInfo(DWARFCompilationUnit cu, BinaryReader reader) throws IOException { try (DWARFLineProgramExecutor lpe = getLineProgramexecutor(cu, reader)) { List results = new ArrayList<>(); for (DWARFLineProgramState row : lpe.allRows()) { - results.add(new SourceFileAddr(row.address, getFilePath(row.file, true), row.line)); + byte[] md5 = null; + if (cu.getDWARFVersion() >= 5 && (row.file < cu.getLine().getNumFiles())) { + md5 = cu.getLine().getFile(row.file).getMD5(); + } + results.add(new SourceFileAddr(row.address, getFilePath(row.file, true), md5, + row.line, row.isEndSequence)); } return results; @@ -315,6 +321,14 @@ public class DWARFLine { "Invalid file index %d for line table at 0x%x: ".formatted(index, startOffset)); } + /** + * Returns the number of indexed files + * @return num files + */ + public int getNumFiles() { + return files.size(); + } + public String getFilePath(int index, boolean includePath) { try { DWARFFile f = getFile(index); @@ -322,9 +336,7 @@ public class DWARFLine { return f.getName(); } - String dir = f.getDirectoryIndex() >= 0 - ? getDir(f.getDirectoryIndex()).getName() - : ""; + String dir = f.getDirectoryIndex() >= 0 ? getDir(f.getDirectoryIndex()).getName() : ""; return FSUtilities.appendPath(dir, f.getName()); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/SourceMapFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/SourceMapFieldFactory.java new file mode 100644 index 0000000000..880504f2c7 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/SourceMapFieldFactory.java @@ -0,0 +1,259 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.viewer.field; + +import java.awt.Color; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import docking.widgets.fieldpanel.field.*; +import docking.widgets.fieldpanel.support.FieldLocation; +import generic.theme.GThemeDefaults.Colors.Palette; +import ghidra.GhidraOptions; +import ghidra.app.util.ListingHighlightProvider; +import ghidra.app.util.viewer.format.FieldFormatModel; +import ghidra.app.util.viewer.proxy.ProxyObj; +import ghidra.framework.options.Options; +import ghidra.framework.options.ToolOptions; +import ghidra.program.database.sourcemap.SourceFile; +import ghidra.program.database.sourcemap.SourceFileIdType; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.CodeUnit; +import ghidra.program.model.sourcemap.*; +import ghidra.program.util.ProgramLocation; +import ghidra.program.util.SourceMapFieldLocation; +import ghidra.util.HelpLocation; + +/** + * {@link FieldFactory} for showing source and line information in the Listing. + */ +public class SourceMapFieldFactory extends FieldFactory { + + static final String FIELD_NAME = "Source Map"; + private static final String GROUP_TITLE = "Source Map"; + static final String SHOW_FILENAME_ONLY_OPTION_NAME = + GROUP_TITLE + Options.DELIMITER + "Show Filename Only"; + static final String SHOW_INFO_AT_ALL_ADDRESSES_OPTION_NAME = + GROUP_TITLE + Options.DELIMITER + "Show Source Info at Every Address"; + static final String MAX_ENTRIES_PER_ADDRESS_OPTION_NAME = + GROUP_TITLE + Options.DELIMITER + "Maximum Number of Source Map Entries to Display"; + static final String SHOW_IDENTIFIER_OPTION_NAME = + GROUP_TITLE + Options.DELIMITER + "Show Identifier"; + static final String NO_SOURCE_INFO = "unknown:??"; + + private boolean showOnlyFileNames = true; + private boolean showInfoAtAllAddresses = false; + private boolean showIdentifier = false; + private static final int DEFAULT_MAX_ENTRIES = 4; + private int maxEntries = DEFAULT_MAX_ENTRIES; + static Color OFFCUT_COLOR = Palette.GRAY; + + /** + * Default constructor + */ + public SourceMapFieldFactory() { + super(FIELD_NAME); + } + + protected SourceMapFieldFactory(FieldFormatModel model, + ListingHighlightProvider highlightProvider, Options displayOptions, + Options fieldOptions) { + super(FIELD_NAME, model, highlightProvider, displayOptions, fieldOptions); + registerOptions(fieldOptions); + } + + @Override + public FieldFactory newInstance(FieldFormatModel formatModel, + ListingHighlightProvider highlightProvider, ToolOptions options, + ToolOptions fieldOptions) { + return new SourceMapFieldFactory(formatModel, highlightProvider, options, fieldOptions); + } + + @Override + public ListingField getField(ProxyObj obj, int varWidth) { + if (!enabled) { + return null; + } + if (!(obj.getObject() instanceof CodeUnit cu)) { + return null; + } + + List entriesToShow = getSourceMapEntries(cu); + if (entriesToShow.isEmpty()) { + if (!showInfoAtAllAddresses) { + return null; + } + AttributedString attrString = + new AttributedString(NO_SOURCE_INFO, Palette.BLACK, getMetrics()); + return ListingTextField.createSingleLineTextField(this, obj, + new TextFieldElement(attrString, 0, 0), startX + varWidth, width, hlProvider); + } + List fieldElements = new ArrayList<>(); + + Address cuAddr = cu.getAddress(); + if (!showInfoAtAllAddresses) { + List entriesStartingWithinCu = new ArrayList<>(); + for (SourceMapEntry entry : entriesToShow) { + if (entry.getBaseAddress().compareTo(cuAddr) >= 0) { + entriesStartingWithinCu.add(entry); + } + } + if (entriesStartingWithinCu.isEmpty()) { + return null; + } + entriesToShow = entriesStartingWithinCu; + } + + for (SourceMapEntry entry : entriesToShow) { + StringBuilder sb = new StringBuilder(); + if (showOnlyFileNames) { + sb.append(entry.getSourceFile().getFilename()); + } + else { + sb.append(entry.getSourceFile().getPath()); + } + sb.append(":"); + sb.append(entry.getLineNumber()); + sb.append(" ("); + sb.append(entry.getLength()); + sb.append(")"); + if (showIdentifier) { + SourceFile sourceFile = entry.getSourceFile(); + if (sourceFile.getIdType().equals(SourceFileIdType.NONE)) { + sb.append(" [no id]"); + } + else { + sb.append(" ["); + sb.append(sourceFile.getIdType().name()); + sb.append("="); + sb.append(sourceFile.getIdAsString()); + sb.append("]"); + } + } + // use gray for entries which start "within" the code unit + Color color = + entry.getBaseAddress().compareTo(cuAddr) <= 0 ? Palette.BLACK : OFFCUT_COLOR; + AttributedString attrString = new AttributedString(sb.toString(), color, getMetrics()); + fieldElements.add(new TextFieldElement(attrString, 0, 0)); + } + + return ListingTextField.createMultilineTextField(this, obj, fieldElements, + startX + varWidth, width, maxEntries, hlProvider); + } + + @Override + public FieldLocation getFieldLocation(ListingField bf, BigInteger index, int fieldNum, + ProgramLocation loc) { + if (loc instanceof SourceMapFieldLocation sourceField) { + return new FieldLocation(index, fieldNum, sourceField.getRow(), + sourceField.getCharOffset()); + } + return null; + } + + @Override + public ProgramLocation getProgramLocation(int row, int col, ListingField bf) { + Object obj = bf.getProxy().getObject(); + if (!(obj instanceof CodeUnit cu)) { + return null; + } + List entriesToShow = getSourceMapEntries(cu); + if (entriesToShow == null || entriesToShow.size() <= row) { + return null; + } + return new SourceMapFieldLocation(cu.getProgram(), cu.getAddress(), row, col, + entriesToShow.get(row)); + } + + @Override + public boolean acceptsType(int category, Class proxyObjectClass) { + return (category == FieldFormatModel.INSTRUCTION_OR_DATA); + } + + @Override + public void fieldOptionsChanged(Options options, String optionName, Object oldValue, + Object newValue) { + super.fieldOptionsChanged(options, optionName, oldValue, newValue); + + if (options.getName().equals(GhidraOptions.CATEGORY_BROWSER_FIELDS)) { + if (optionName.equals(SHOW_FILENAME_ONLY_OPTION_NAME)) { + showOnlyFileNames = options.getBoolean(SHOW_FILENAME_ONLY_OPTION_NAME, true); + } + if (optionName.equals(SHOW_INFO_AT_ALL_ADDRESSES_OPTION_NAME)) { + showInfoAtAllAddresses = + options.getBoolean(SHOW_INFO_AT_ALL_ADDRESSES_OPTION_NAME, false); + } + if (optionName.equals(MAX_ENTRIES_PER_ADDRESS_OPTION_NAME)) { + maxEntries = + options.getInt(MAX_ENTRIES_PER_ADDRESS_OPTION_NAME, DEFAULT_MAX_ENTRIES); + } + if (optionName.equals(SHOW_IDENTIFIER_OPTION_NAME)) { + showIdentifier = options.getBoolean(SHOW_IDENTIFIER_OPTION_NAME, false); + } + model.update(); + } + } + + private void registerOptions(Options fieldOptions) { + HelpLocation helpLoc = new HelpLocation("CodeBrowserPlugin", "Source_Map_Field"); + + fieldOptions.registerOption(SHOW_FILENAME_ONLY_OPTION_NAME, true, helpLoc, + "Show only source file name (rather than absolute path)"); + showOnlyFileNames = fieldOptions.getBoolean(SHOW_FILENAME_ONLY_OPTION_NAME, true); + + fieldOptions.registerOption(SHOW_INFO_AT_ALL_ADDRESSES_OPTION_NAME, false, helpLoc, + "Show source info at every address " + + "(rather than only at beginning of source map entries)"); + showInfoAtAllAddresses = + fieldOptions.getBoolean(SHOW_INFO_AT_ALL_ADDRESSES_OPTION_NAME, false); + + fieldOptions.registerOption(MAX_ENTRIES_PER_ADDRESS_OPTION_NAME, 4, helpLoc, + "Maximum number of source map entries to display"); + maxEntries = fieldOptions.getInt(MAX_ENTRIES_PER_ADDRESS_OPTION_NAME, DEFAULT_MAX_ENTRIES); + + fieldOptions.registerOption(SHOW_IDENTIFIER_OPTION_NAME, false, helpLoc, + "Show source file identifier info"); + showIdentifier = fieldOptions.getBoolean(SHOW_IDENTIFIER_OPTION_NAME, false); + + } + + private List getSourceMapEntries(CodeUnit cu) { + List entries = new ArrayList<>(); + SourceFileManager sourceManager = cu.getProgram().getSourceFileManager(); + // check all addresses in the code unit to handle the (presumably rare) case where + // there is an entry associated with an address in the code unit which is not its + // minimum address + Address cuMinAddr = cu.getMinAddress(); + Address cuMaxAddr = cu.getMaxAddress(); + SourceMapEntryIterator entryIter = + sourceManager.getSourceMapEntryIterator(cuMaxAddr, false); + while (entryIter.hasNext()) { + SourceMapEntry entry = entryIter.next(); + long entryLength = entry.getLength(); + long adjusted = entryLength == 0 ? 0 : entryLength - 1; + if (entry.getBaseAddress().add(adjusted).compareTo(cuMinAddr) >= 0) { + entries.add(entry); + continue; + } + if (entryLength != 0) { + break; + } + } + return entries.reversed(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/program/util/ProgramDiff.java b/Ghidra/Features/Base/src/main/java/ghidra/program/util/ProgramDiff.java index 2170016a51..e9c4513920 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/program/util/ProgramDiff.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/program/util/ProgramDiff.java @@ -19,12 +19,16 @@ import java.util.*; import javax.help.UnsupportedOperationException; +import org.apache.commons.collections4.CollectionUtils; import ghidra.program.database.properties.UnsupportedMapDB; +import ghidra.program.database.sourcemap.SourceFile; import ghidra.program.model.address.*; import ghidra.program.model.lang.Register; import ghidra.program.model.lang.RegisterValue; import ghidra.program.model.listing.*; import ghidra.program.model.mem.*; +import ghidra.program.model.sourcemap.SourceFileManager; +import ghidra.program.model.sourcemap.SourceMapEntry; import ghidra.program.model.symbol.*; import ghidra.program.model.util.PropertyMap; import ghidra.program.model.util.PropertyMapManager; @@ -983,6 +987,11 @@ public class ProgramDiff { monitor.setMessage(monitorMsg); as = getFunctionTagDifferences(checkAddressSet, monitor); break; + case ProgramDiffFilter.SOURCE_MAP_DIFFS: + monitorMsg = "Checking Source Map Differences"; + monitor.setMessage(monitorMsg); + as = getSourceMapDifferences(checkAddressSet, monitor); + break; } if (as != null) { diffAddrSets.put(diffType, as); @@ -1566,6 +1575,199 @@ public class ProgramDiff { return c.getObjectDiffs(iter1, iter2, monitor); } + /** + * Determines the source map differences for the addresses in {@code addressSet}. Source map + * entries which intersect but are not contained within {@code addressSet} are ignored. + * The returned {@link AddressSet} consists of the minimum addresses of all of the differing + * source map entries. This method does not check for differences between non-mapped + * {@link SourceFile}s. + * + * @param addressSet addresses to check for differences (from program1) + * @param monitor task monitor + * @return minimum addresses of differing ranges + * @throws CancelledException if user cancels + */ + private AddressSet getSourceMapDifferences(AddressSetView addressSet, TaskMonitor monitor) + throws CancelledException { + SourceFileManager p1Manager = program1.getSourceFileManager(); + SourceFileManager p2Manager = program2.getSourceFileManager(); + + List p1Sources = program1.getSourceFileManager().getMappedSourceFiles(); + List p2Sources = program2.getSourceFileManager().getMappedSourceFiles(); + + Collection differingPaths = CollectionUtils.disjunction(p1Sources, p2Sources); + + Collection p1Only = CollectionUtils.intersection(p1Sources, differingPaths); + Collection p2Only = CollectionUtils.intersection(p2Sources, differingPaths); + + AddressSet differences = + processDifferingSourceFiles(addressSet, p1Only, p2Only, p1Manager, p2Manager, monitor); + + // now consider the mapping info for the source files the programs have in common + Collection commonSources = CollectionUtils.intersection(p1Sources, p2Sources); + + // first check the entries in program1 + // only need to check an address once, even if there are multiple entries based at it + AddressSet p1CheckedAddresses = new AddressSet(); + AddressSet p2CheckedAddresses = new AddressSet(); + + for (SourceFile common : commonSources) { + for (SourceMapEntry p1Entry : p1Manager.getSourceMapEntries(common)) { + monitor.checkCancelled(); + Address p1Base = p1Entry.getBaseAddress(); + if (p1CheckedAddresses.contains(p1Base)) { + continue; + } + + Address p2Base = SimpleDiffUtility.getCompatibleAddress(program1, p1Base, program2); + if (p2Base == null) { + p1CheckedAddresses.add(p1Base); + continue; + } + + long p1Length = p1Entry.getLength(); + if (p1Length != 0) { + Address p1End = p1Base.add(p1Length - 1); + if (!addressSet.contains(p1Base, p1End)) { + if (addressSet.intersects(p1Base, p1End)) { + logSkippedEntry(p1Entry, program1); + } + continue; + } + } + p1CheckedAddresses.add(p1Base); + p2CheckedAddresses.add(p2Base); + List p1Entries = p1Manager.getSourceMapEntries(p1Base); + List p2Entries = p2Manager.getSourceMapEntries(p2Base); + if (p1Entries.size() != p2Entries.size()) { + differences.add(p1Base); + continue; + } + for (SourceMapEntry e1 : p1Entries) { + if (Collections.binarySearch(p2Entries, e1) < 0) { + differences.add(p1Base); + break; + } + } + } + } + + AddressSet unmatchedP2Addrs = + findUnmatchedP2Addrs(addressSet, p2CheckedAddresses, commonSources, monitor); + differences.add(unmatchedP2Addrs); + + return differences; + + } + + // look for addresses in program2 that have entries where the corresponding + // address in program1 does not have any entries + private AddressSet findUnmatchedP2Addrs(AddressSetView addressSet, + AddressSet p2CheckedAddresses, Collection commonSources, + TaskMonitor monitor) throws CancelledException { + AddressSet results = new AddressSet(); + SourceFileManager p2Manager = program2.getSourceFileManager(); + for (SourceFile p2Source : commonSources) { + for (SourceMapEntry p2Entry : p2Manager.getSourceMapEntries(p2Source)) { + monitor.checkCancelled(); + Address p2Base = p2Entry.getBaseAddress(); + if (p2CheckedAddresses.contains(p2Base)) { + continue; + } + Address p1Base = SimpleDiffUtility.getCompatibleAddress(program2, p2Base, program1); + p2CheckedAddresses.add(p2Base); + if (p1Base == null) { + continue; + } + if (!addressSet.contains(p1Base)) { + continue; + } + List p1Entries = + program1.getSourceFileManager().getSourceMapEntries(p1Base); + boolean p1BaseAlreadyChecked = false; + for (SourceMapEntry p1Entry : p1Entries) { + if (p1Entry.getBaseAddress().equals(p1Base)) { + p1BaseAlreadyChecked = true; + break; + } + } + if (!p1BaseAlreadyChecked) { + results.add(p1Base); + } + } + } + return results; + } + + // process SourceFiles with mapping info in exactly one program + // any associated SourceMapEntry must be a difference + // so just check that the range is part of addressSet + private AddressSet processDifferingSourceFiles(AddressSetView addressSet, + Collection p1Only, Collection p2Only, + SourceFileManager p1Manager, SourceFileManager p2Manager, TaskMonitor monitor) + throws CancelledException { + AddressSet result = new AddressSet(); + + for (SourceFile p1Source : p1Only) { + List p1Entries = p1Manager.getSourceMapEntries(p1Source); + for (SourceMapEntry p1Entry : p1Entries) { + monitor.checkCancelled(); + Address p1Base = p1Entry.getBaseAddress(); + if (p1Entry.getLength() == 0) { + if (addressSet.contains(p1Base)) { + result.add(p1Base); + } + continue; + } + Address p1End = p1Base.add(p1Entry.getLength() - 1); + if (addressSet.contains(p1Base, p1End)) { + result.add(p1Base); + continue; + } + if (addressSet.intersects(p1Base, p1End)) { + logSkippedEntry(p1Entry, program1); + } + } + } + + for (SourceFile p2Source : p2Only) { + List p2Entries = p2Manager.getSourceMapEntries(p2Source); + for (SourceMapEntry p2Entry : p2Entries) { + monitor.checkCancelled(); + Address p2Base = p2Entry.getBaseAddress(); + Address p1Base = SimpleDiffUtility.getCompatibleAddress(program2, p2Base, program1); + if (p1Base == null) { + continue; + } + long p2Length = p2Entry.getLength(); + if (p2Length == 0) { + if (addressSet.contains(p1Base)) { + result.add(p1Base); + } + continue; + } + Address p2End = p2Base.add(p2Length - 1); + Address p1End = SimpleDiffUtility.getCompatibleAddress(program2, p2End, program1); + if (p1End == null) { + continue; + } + if (addressSet.contains(p1Base, p1End)) { + result.add(p1Base); + continue; + } + if (addressSet.intersects(p1Base, p1End)) { + logSkippedEntry(p2Entry, program2); + } + } + } + return result; + } + + private void logSkippedEntry(SourceMapEntry entry, Program program) { + Msg.warn(this, + "Skipping source map entry " + entry.toString() + " in program " + program.getName()); + } + /** * Determines the code unit addresses where there are differences of the * indicated type between program1 and program2. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/program/util/ProgramDiffFilter.java b/Ghidra/Features/Base/src/main/java/ghidra/program/util/ProgramDiffFilter.java index fa7d990014..63a6fe1dc1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/program/util/ProgramDiffFilter.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/program/util/ProgramDiffFilter.java @@ -23,29 +23,38 @@ package ghidra.program.util; * differences of that type between two programs. False indicates no interest * in this type of program difference. *
Valid filter types are: - * BYTE_DIFFS, CODE_UNIT_DIFFS, - * PLATE_COMMENT_DIFFS, PRE_COMMENT_DIFFS, EOL_COMMENT_DIFFS, - * REPEATABLE_COMMENT_DIFFS, POST_COMMENT_DIFFS, + * BOOKMARK_DIFFS, + * BYTE_DIFFS, + * CODE_UNIT_DIFFS, + * EQUATE_DIFFS, + * EOL_COMMENT_DIFFS, + * FUNCTION_DIFFS, + * FUNCTION_TAG_DIFFS, + * PLATE_COMMENT_DIFFS, + * POST_COMMENT_DIFFS, + * PRE_COMMENT_DIFFS, + * PROGRAM_CONTEXT_DIFFS, * REFERENCE_DIFFS, - * USER_DEFINED_DIFFS, BOOKMARK_DIFFS, + * REPEATABLE_COMMENT_DIFFS, + * SOURCE_MAP_DIFFS, * SYMBOL_DIFFS, - * EQUATE_DIFFS, FUNCTION_DIFFS, PROGRAM_CONTEXT_DIFFS. + * USER_DEFINED_DIFFS. *
Predefined filter type combinations are: * COMMENT_DIFFS and ALL_DIFFS. */ public class ProgramDiffFilter { - //////////////////////////////////////////////////////////////////////// - // The following are definitions for the different types of filtering. - // Combination filter indicators are created by "OR"ing the individual - // filter indicators. - //////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////// + // The following are definitions for the different types of filtering. + // Combination filter indicators are created by "OR"ing the individual + // filter indicators. + //////////////////////////////////////////////////////////////////////// /** Indicates the filter for the program context (register) differences. */ public static final int PROGRAM_CONTEXT_DIFFS = 1 << 0; - /** Indicates the filter for the byte differences. */ - public static final int BYTE_DIFFS = 1 << 1; - /** Indicates the filter for the code unit differences. */ - public static final int CODE_UNIT_DIFFS = 1 << 2; + /** Indicates the filter for the byte differences. */ + public static final int BYTE_DIFFS = 1 << 1; + /** Indicates the filter for the code unit differences. */ + public static final int CODE_UNIT_DIFFS = 1 << 2; /** Indicates the filter for the end of line comment differences. */ public static final int EOL_COMMENT_DIFFS = 1 << 3; /** Indicates the filter for the pre comment differences. */ @@ -60,8 +69,8 @@ public class ProgramDiffFilter { public static final int REFERENCE_DIFFS = 1 << 8; /** Indicates the filter for the equates differences. */ public static final int EQUATE_DIFFS = 1 << 9; - /** Indicates the filter for the symbol differences. */ - public static final int SYMBOL_DIFFS = 1 << 10; + /** Indicates the filter for the symbol differences. */ + public static final int SYMBOL_DIFFS = 1 << 10; /** Indicates the filter for the function differences. */ public static final int FUNCTION_DIFFS = 1 << 11; /** Indicates the filter for bookmark differences. */ @@ -70,23 +79,21 @@ public class ProgramDiffFilter { public static final int USER_DEFINED_DIFFS = 1 << 13; /** Indicates the filter for the function tag differences. */ public static final int FUNCTION_TAG_DIFFS = 1 << 14; - - // NOTE: If you add a new primary type here, make sure to use the - // next available bit position and update the NUM_PRIMARY_TYPES. - // ** Also don't forget to add it to ALL_DIFFS below. ** - /** The total number of primary difference types. */ - private static final int NUM_PRIMARY_TYPES = 15; + /** Indicates the filter for source map differences */ + public static final int SOURCE_MAP_DIFFS = 1 << 15; - //******************************************************** - //* PREDEFINED DIFFERENCE COMBINATIONS - //******************************************************** + // NOTE: If you add a new primary type here, make sure to use the + // next available bit position and update the NUM_PRIMARY_TYPES. + // ** Also don't forget to add it to ALL_DIFFS below. ** + /** The total number of primary difference types. */ + private static final int NUM_PRIMARY_TYPES = 16; + + //******************************************************** + //* PREDEFINED DIFFERENCE COMBINATIONS + //******************************************************** /** Indicates all comment filters. */ - public static final int COMMENT_DIFFS = - EOL_COMMENT_DIFFS - | PRE_COMMENT_DIFFS - | POST_COMMENT_DIFFS - | REPEATABLE_COMMENT_DIFFS - | PLATE_COMMENT_DIFFS; + public static final int COMMENT_DIFFS = EOL_COMMENT_DIFFS | PRE_COMMENT_DIFFS | + POST_COMMENT_DIFFS | REPEATABLE_COMMENT_DIFFS | PLATE_COMMENT_DIFFS; /** Indicates all filters for all defined types of differences. */ //@formatter:off public static final int ALL_DIFFS = @@ -100,18 +107,18 @@ public class ProgramDiffFilter { | FUNCTION_DIFFS | BOOKMARK_DIFFS | FUNCTION_TAG_DIFFS - | PROGRAM_CONTEXT_DIFFS; + | PROGRAM_CONTEXT_DIFFS + | SOURCE_MAP_DIFFS; //@formatter:on - /** filterFlags holds the actual indicators for each - * difference type as a bit setting. - */ - private int filterFlags = 0; + /** filterFlags holds the actual indicators for each + * difference type as a bit setting. + */ + private int filterFlags = 0; - - /** Creates new ProgramDiffFilter with none of the diff types selected.*/ - public ProgramDiffFilter() { - } + /** Creates new ProgramDiffFilter with none of the diff types selected.*/ + public ProgramDiffFilter() { + } /** Creates new ProgramDiffFilter equivalent to the specified ProgramDiffFilter. * @@ -121,47 +128,47 @@ public class ProgramDiffFilter { this.filterFlags = filter.filterFlags; } - /** Creates new ProgramDiffFilter with the specified diff types selected. - * - * @param type one or more of the diff types "OR"ed together. - *
i.e. CODE_UNIT_DIFFS | SYMBOL_DIFFS - */ - public ProgramDiffFilter(int type) { - filterFlags = ALL_DIFFS & type; - } + /** Creates new ProgramDiffFilter with the specified diff types selected. + * + * @param type one or more of the diff types "OR"ed together. + *
i.e. CODE_UNIT_DIFFS | SYMBOL_DIFFS + */ + public ProgramDiffFilter(int type) { + filterFlags = ALL_DIFFS & type; + } - /** - * getFilter determines whether or not the specified type of filter is set. - * - * @param type the set bits indicate the type of differences we want to - * check as being set in the filter. - *
For example, one or more of the diff types "OR"ed together. - *
i.e. CODE_UNIT_DIFFS | SYMBOL_DIFFS - * @return true if filtering for the specified type of differences. - */ - public boolean getFilter(int type) { - return ((type & filterFlags) != 0); - } + /** + * getFilter determines whether or not the specified type of filter is set. + * + * @param type the set bits indicate the type of differences we want to + * check as being set in the filter. + *
For example, one or more of the diff types "OR"ed together. + *
i.e. CODE_UNIT_DIFFS | SYMBOL_DIFFS + * @return true if filtering for the specified type of differences. + */ + public boolean getFilter(int type) { + return ((type & filterFlags) != 0); + } - /** set this filter to look for types of differences in addition to those - * types where it is already looking for differences. - * The filter that is passed as a parameter indicates the additional types - * of differences. - * - * @param filter filter indicating the additional types of differences - * to look for between the programs. - */ - synchronized public void addToFilter(ProgramDiffFilter filter) { - filterFlags |= filter.filterFlags; - } + /** set this filter to look for types of differences in addition to those + * types where it is already looking for differences. + * The filter that is passed as a parameter indicates the additional types + * of differences. + * + * @param filter filter indicating the additional types of differences + * to look for between the programs. + */ + synchronized public void addToFilter(ProgramDiffFilter filter) { + filterFlags |= filter.filterFlags; + } /** setFilter specifies whether or not the indicated type of difference will be * included by the filter (true) or not included (false). * - * @param type the set bits indicate the type of differences we want to - * look for in the programs. - *
For example, one or more of the diff types "OR"ed together. - *
i.e. CODE_UNIT_DIFFS | SYMBOL_DIFFS + * @param type the set bits indicate the type of differences we want to + * look for in the programs. + *
For example, one or more of the diff types "OR"ed together. + *
i.e. CODE_UNIT_DIFFS | SYMBOL_DIFFS * @param filter true if you want to determine differences of the specified type. */ synchronized public void setFilter(int type, boolean filter) { @@ -173,47 +180,47 @@ public class ProgramDiffFilter { } } - /** - * Sets all the defined types of differences to false. - * Filter indicates no interest in any difference types. - */ - public void clearAll() { - setFilter(ALL_DIFFS, false); - } + /** + * Sets all the defined types of differences to false. + * Filter indicates no interest in any difference types. + */ + public void clearAll() { + setFilter(ALL_DIFFS, false); + } - /** - * Sets all the defined types of differences to true. - * Filter indicates interest in all difference types. - */ - public void selectAll() { - setFilter(ALL_DIFFS, true); - } + /** + * Sets all the defined types of differences to true. + * Filter indicates interest in all difference types. + */ + public void selectAll() { + setFilter(ALL_DIFFS, true); + } - /** - * Gets all the valid individual types of differences for this filter. - * These are also referred to as primary difference types. - * @return an array containing all the currently defined difference types - */ - public static int[] getPrimaryTypes() { - int[] pt = new int[NUM_PRIMARY_TYPES]; - for (int i=0; itypeToName() returns the name of the difference type. - * Only predefined types, as specified in ProgramDiffFilter, - * will return a name. Otherwise, an empty string is returned. - * @param type the type of difference whose name is wanted. - * @return the name of the predefined difference type. Otherwise, the empty string. - */ - public static String typeToName(int type) { - switch (type) { - case ProgramDiffFilter.BYTE_DIFFS: - return "BYTE_DIFFS"; - case ProgramDiffFilter.CODE_UNIT_DIFFS: - return "CODE_UNIT_DIFFS"; + /** typeToName() returns the name of the difference type. + * Only predefined types, as specified in ProgramDiffFilter, + * will return a name. Otherwise, an empty string is returned. + * @param type the type of difference whose name is wanted. + * @return the name of the predefined difference type. Otherwise, the empty string. + */ + public static String typeToName(int type) { + switch (type) { + case ProgramDiffFilter.BYTE_DIFFS: + return "BYTE_DIFFS"; + case ProgramDiffFilter.CODE_UNIT_DIFFS: + return "CODE_UNIT_DIFFS"; case ProgramDiffFilter.COMMENT_DIFFS: return "COMMENT_DIFFS"; case ProgramDiffFilter.EOL_COMMENT_DIFFS: @@ -226,53 +233,55 @@ public class ProgramDiffFilter { return "PLATE_COMMENT_DIFFS"; case ProgramDiffFilter.REPEATABLE_COMMENT_DIFFS: return "REPEATABLE_COMMENT_DIFFS"; - case ProgramDiffFilter.REFERENCE_DIFFS: - return "REFERENCE_DIFFS"; - case ProgramDiffFilter.USER_DEFINED_DIFFS: - return "USER_DEFINED_DIFFS"; + case ProgramDiffFilter.REFERENCE_DIFFS: + return "REFERENCE_DIFFS"; + case ProgramDiffFilter.USER_DEFINED_DIFFS: + return "USER_DEFINED_DIFFS"; case ProgramDiffFilter.SYMBOL_DIFFS: return "SYMBOL_DIFFS"; - case ProgramDiffFilter.EQUATE_DIFFS: - return "EQUATE_DIFFS"; + case ProgramDiffFilter.EQUATE_DIFFS: + return "EQUATE_DIFFS"; case ProgramDiffFilter.FUNCTION_DIFFS: return "FUNCTION_DIFFS"; case ProgramDiffFilter.BOOKMARK_DIFFS: return "BOOKMARK_DIFFS"; - case ProgramDiffFilter.PROGRAM_CONTEXT_DIFFS: - return "PROGRAM_CONTEXT_DIFFS"; - case ProgramDiffFilter.ALL_DIFFS: - return "ALL_DIFFS"; + case ProgramDiffFilter.PROGRAM_CONTEXT_DIFFS: + return "PROGRAM_CONTEXT_DIFFS"; + case ProgramDiffFilter.ALL_DIFFS: + return "ALL_DIFFS"; case ProgramDiffFilter.FUNCTION_TAG_DIFFS: return "FUNCTION_TAG_DIFFS"; - default: - return ""; - } - } + case ProgramDiffFilter.SOURCE_MAP_DIFFS: + return "SOURCE_MAP_DIFFS"; + default: + return ""; + } + } - /** - * Determines whether or not this filter is equal to the object that - * is passed in. - * @param obj the object to compare this one with. - * @return true if the filter matches this one. - */ - @Override - public boolean equals(Object obj) { - if (obj instanceof ProgramDiffFilter) { - return ((ProgramDiffFilter)obj).filterFlags == filterFlags; - } - return false; - } - - /** - * Returns a string representation of the current settings for this filter. - */ - @Override - public String toString() { - StringBuffer buf = new StringBuffer(); - buf.append("ProgramDiffFilter:\n"); - for (int i=0; imerge filter for function tags. */ public static final int FUNCTION_TAGS = 1 << MERGE_FUNCTION_TAGS; + /** Internal array index for "source map" filter value. */ + private static final int MERGE_SOURCE_MAP = 17; + /** Indicates the merge filter for source map information. */ + public static final int SOURCE_MAP = 1 << MERGE_SOURCE_MAP; + // NOTE: If you add a new primary type here, make sure to use the // next available integer and update the NUM_PRIMARY_TYPES. // ** Also don't forget to add it to ALL below. ** /** The total number of primary merge difference types. */ - private static final int NUM_PRIMARY_TYPES = 17; + private static final int NUM_PRIMARY_TYPES = 18; /** Indicates to merge code unit differences. This includes instructions, * data, and equates. @@ -152,12 +157,13 @@ public class ProgramMergeFilter { public static final int CODE_UNITS = INSTRUCTIONS | DATA; /** Indicates to merge all comment differences. */ - public static final int COMMENTS = PLATE_COMMENTS | PRE_COMMENTS | EOL_COMMENTS | - REPEATABLE_COMMENTS | POST_COMMENTS; + public static final int COMMENTS = + PLATE_COMMENTS | PRE_COMMENTS | EOL_COMMENTS | REPEATABLE_COMMENTS | POST_COMMENTS; /** Indicates all merge filters for all types of differences. */ - public static final int ALL = PROGRAM_CONTEXT | BYTES | CODE_UNITS | EQUATES | REFERENCES | - COMMENTS | SYMBOLS | PRIMARY_SYMBOL | BOOKMARKS | PROPERTIES | FUNCTIONS | FUNCTION_TAGS; + public static final int ALL = + PROGRAM_CONTEXT | BYTES | CODE_UNITS | EQUATES | REFERENCES | COMMENTS | SYMBOLS | + PRIMARY_SYMBOL | BOOKMARKS | PROPERTIES | FUNCTIONS | FUNCTION_TAGS | SOURCE_MAP; /** Array holding the filter value for each of the primary merge difference types. */ private int[] filterFlags = new int[NUM_PRIMARY_TYPES]; @@ -185,7 +191,7 @@ public class ProgramMergeFilter { } /** getFilter determines whether or not the specified type of filter is set. - * Valid types are: BYTES, INSTRUCTIONS, DATA, + * Valid types are: BYTES, INSTRUCTIONS, DATA, SOURCE_MAP, * SYMBOLS, PRIMARY_SYMBOL, COMMENTS, PROGRAM_CONTEXT, PROPERTIES, BOOKMARKS, FUNCTIONS. * INVALID is returned if combinations of merge types (e.g. ALL) are * passed in. @@ -213,6 +219,7 @@ public class ProgramMergeFilter { case FUNCTIONS: case FUNCTION_TAGS: case EQUATES: + case SOURCE_MAP: int bitPos = 0; int tmpType = type; while (bitPos < NUM_PRIMARY_TYPES) { @@ -234,7 +241,8 @@ public class ProgramMergeFilter { /** validatePredefinedType determines whether or not the indicated type * of filter item is a valid predefined type. * Valid types are: BYTES, INSTRUCTIONS, DATA, - * SYMBOLS, PRIMARY_SYMBOL, COMMENTS, PROGRAM_CONTEXT, PROPERTIES, BOOKMARKS, FUNCTIONS, ALL. + * SYMBOLS, PRIMARY_SYMBOL, COMMENTS, PROGRAM_CONTEXT, PROPERTIES, BOOKMARKS, FUNCTIONS, + * SOURCE_MAP, ALL. * * @param type the type of difference to look for between the programs. * @return true if this is a pre-defined merge type. @@ -260,6 +268,7 @@ public class ProgramMergeFilter { case EQUATES: case CODE_UNITS: case COMMENTS: + case SOURCE_MAP: case ALL: return true; default: @@ -316,7 +325,8 @@ public class ProgramMergeFilter { /** isMergeValidForFilter determines whether or not the MERGE * filter if valid for the indicated primary merge type. * Possible types are: BYTES, INSTRUCTIONS, DATA, REFERENCES, - * SYMBOLS, PRIMARY_SYMBOL, COMMENTS, PROGRAM_CONTEXT, PROPERTIES, BOOKMARKS, FUNCTIONS, ALL. + * SYMBOLS, PRIMARY_SYMBOL, COMMENTS, PROGRAM_CONTEXT, PROPERTIES, BOOKMARKS, FUNCTIONS, + * SOURCE_MAP, ALL. * * @param type the type of difference to merge between the programs. * @return true if MERGE is valid for the merge type. @@ -325,7 +335,7 @@ public class ProgramMergeFilter { */ private boolean isMergeValidForFilter(int type) throws IllegalArgumentException { switch (type) { - // The following can be MERGE. + // The following can be MERGE. case PLATE_COMMENTS: case PRE_COMMENTS: case EOL_COMMENTS: @@ -334,7 +344,7 @@ public class ProgramMergeFilter { case SYMBOLS: case FUNCTION_TAGS: return true; - // The following cannot be MERGE. + // The following cannot be MERGE. case PROGRAM_CONTEXT: case BYTES: case INSTRUCTIONS: @@ -345,6 +355,7 @@ public class ProgramMergeFilter { case FUNCTIONS: case EQUATES: case PRIMARY_SYMBOL: + case SOURCE_MAP: return false; default: throw new IllegalArgumentException( @@ -469,6 +480,8 @@ public class ProgramMergeFilter { return "COMMENTS"; case ALL: return "ALL"; + case SOURCE_MAP: + return "SOURCE_MAP"; default: return ""; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/program/util/ProgramMergeManager.java b/Ghidra/Features/Base/src/main/java/ghidra/program/util/ProgramMergeManager.java index 909cee56b0..13436edb4f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/program/util/ProgramMergeManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/program/util/ProgramMergeManager.java @@ -15,10 +15,13 @@ */ package ghidra.program.util; +import ghidra.framework.store.LockException; +import ghidra.program.database.sourcemap.SourceFile; import ghidra.program.model.address.*; import ghidra.program.model.listing.Program; import ghidra.program.model.mem.MemoryAccessException; import ghidra.util.Msg; +import ghidra.util.exception.AssertException; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -285,8 +288,8 @@ public class ProgramMergeManager { * @throws MemoryAccessException if bytes can't be copied. * @throws CancelledException if user cancels via the monitor. */ - public boolean merge(Address p2Address, TaskMonitor monitor) throws MemoryAccessException, - CancelledException { + public boolean merge(Address p2Address, TaskMonitor monitor) + throws MemoryAccessException, CancelledException { return merge(p2Address, mergeFilter, monitor); } @@ -451,6 +454,7 @@ public class ProgramMergeManager { mergeBookmarks(p1MergeSet, filter, monitor); mergeProperties(p1MergeSet, filter, monitor); mergeFunctionTags(p1MergeSet, filter, monitor); + mergeSourceMap(p1MergeSet, filter, monitor); merger.reApplyDuplicateEquates(); String dupEquatesMessage = merger.getDuplicateEquatesInfo(); @@ -569,9 +573,8 @@ public class ProgramMergeManager { AddressSet byteDiffs2 = null; ProgramDiffFilter byteDiffFilter = new ProgramDiffFilter(ProgramDiffFilter.BYTE_DIFFS); if (filter.getFilter(ProgramMergeFilter.BYTES) == ProgramMergeFilter.IGNORE) { - byteDiffs2 = - DiffUtility.getCompatibleAddressSet( - programDiff.getDifferences(byteDiffFilter, monitor), program2); + byteDiffs2 = DiffUtility.getCompatibleAddressSet( + programDiff.getDifferences(byteDiffFilter, monitor), program2); } // ** Equates ** @@ -628,6 +631,46 @@ public class ProgramMergeManager { } } + /** + * Merges source map information from program 2 into program 1. + *
+ * Note: This method does not consider unmapped {@link SourceFile}s, i.e., those which are + * not associated with any addresses in program 2. In particular, it will not create new + * unmapped files in program 1. + * + * @param p1AddressSet address set in program 1 to receive changes + * @param filter merge filter + * @param monitor monitor + */ + void mergeSourceMap(AddressSetView p1AddressSet, ProgramMergeFilter filter, + TaskMonitor monitor) { + int setting = filter.getFilter(ProgramMergeFilter.SOURCE_MAP); + if (setting == ProgramMergeFilter.IGNORE) { + return; + } + if (setting == ProgramMergeFilter.MERGE) { + throw new IllegalStateException("Cannot merge source map information"); + } + int diffType = ProgramDiffFilter.SOURCE_MAP_DIFFS; + try { + AddressSetView p1DiffSet = + programDiff.getDifferences(new ProgramDiffFilter(diffType), monitor); + AddressSet p1MergeSet = p1DiffSet.intersect(p1AddressSet); + AddressSet p2MergeSet = DiffUtility.getCompatibleAddressSet(p1MergeSet, program2); + + merger.applySourceMapDifferences(p2MergeSet, setting, monitor); + + } + catch (CancelledException e1) { + // user cancellation + } + catch (LockException e) { + // GUI prevents you from selecting "REPLACE" if you don't have exclusive access + // so we shouldn't get here + throw new AssertException("Attempting to merge source map without exclusive access"); + } + } + /** * mergeComments merges all comments * in the specified address set from the second program diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/SourceFileUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/util/SourceFileUtils.java new file mode 100644 index 0000000000..bff9d2c53b --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/SourceFileUtils.java @@ -0,0 +1,174 @@ +/* ### + * 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; + +import java.net.URI; +import java.util.HexFormat; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import ghidra.formats.gfilesystem.FSUtilities; +import ghidra.program.database.sourcemap.SourceFile; +import ghidra.program.database.sourcemap.SourceFileIdType; +import ghidra.program.model.listing.Program; +import ghidra.program.model.sourcemap.SourceMapEntry; + +/** + * A utility class for creating {@link SourceFile}s from native paths, e.g., windows paths. + */ +public class SourceFileUtils { + + private static HexFormat hexFormat = HexFormat.of(); + + private SourceFileUtils() { + // singleton class + } + + /** + * Creates a {@link SourceFile} from {@code path} with id type {@link SourceFileIdType#NONE} + * and empty identifier. The path will be transformed using + * {@link FSUtilities#normalizeNativePath(String)} and then {@link URI#normalize}. + * + * @param path path + * @return source file + */ + public static SourceFile getSourceFileFromPathString(String path) { + return getSourceFileFromPathString(path, SourceFileIdType.NONE, null); + } + + /** + * Creates a {@link SourceFile} from {@code path} with the provided id type and identifier. + * The path will be transformed using{@link FSUtilities#normalizeNativePath(String)} and + * then {@link URI#normalize}. + * + * @param path path + * @param idType id type + * @param identifier identifier + * @return source file + */ + public static SourceFile getSourceFileFromPathString(String path, SourceFileIdType idType, + byte[] identifier) { + String standardized = FSUtilities.normalizeNativePath(path); + return new SourceFile(standardized, idType, identifier); + } + + /** + * Converts a {@code long} value to an byte array of length 8. The most significant byte + * of the long will be at position 0 of the resulting array. + * @param l long + * @return byte array + */ + public static byte[] longToByteArray(long l) { + byte[] bytes = new byte[8]; + BigEndianDataConverter.INSTANCE.putLong(bytes, 0, l); + return bytes; + } + + /** + * Converts a byte array of length 8 to a {@code long} value. The byte at position 0 + * of the array will be the most significant byte of the resulting long. + * @param bytes array to convert + * @return long + * @throws IllegalArgumentException if bytes.length != 8 + */ + public static long byteArrayToLong(byte[] bytes) { + if (bytes.length != 8) { + throw new IllegalArgumentException("bytes must have length 8"); + } + return BigEndianDataConverter.INSTANCE.getLong(bytes); + } + + /** + * Converts a {@code String} of hexadecimal character to an array of bytes. An initial "0x" + * or "0X" is ignored, as is the case of the digits a-f. + * @param hexString String to convert + * @return byte array + */ + public static byte[] hexStringToByteArray(String hexString) { + if (StringUtils.isBlank(hexString)) { + return new byte[0]; + } + if (hexString.startsWith("0x") || hexString.startsWith("0X")) { + hexString = hexString.substring(2); + } + return hexFormat.parseHex(hexString); + } + + /** + * Converts a byte array to a {@code String} of hexadecimal digits. + * @param bytes array to convert + * @return string + */ + public static String byteArrayToHexString(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return StringUtils.EMPTY; + } + return hexFormat.formatHex(bytes); + } + + /** + * Returns a {@link SourceLineBounds} record containing the minimum and maximum mapped line + * for {@code sourceFile} in {@code program}. + * @param program program + * @param sourceFile source file + * @return source line bounds or null + */ + public static SourceLineBounds getSourceLineBounds(Program program, SourceFile sourceFile) { + List entries = + program.getSourceFileManager().getSourceMapEntries(sourceFile, 0, Integer.MAX_VALUE); + if (entries.isEmpty()) { + return null; + } + int min = Integer.MAX_VALUE; + int max = -1; + for (SourceMapEntry entry : entries) { + int lineNum = entry.getLineNumber(); + if (lineNum < min) { + min = lineNum; + } + if (lineNum > max) { + max = lineNum; + } + } + return new SourceLineBounds(min, max); + } + + /** + * A record containing the minimum and maximum mapped line numbers + * @param min minimum line number + * @param max maximum line number + */ + public static record SourceLineBounds(int min, int max) { + + public SourceLineBounds(int min, int max) { + if (min < 0) { + throw new IllegalArgumentException("min must be greater than or equal to 0"); + } + if (max < 0) { + throw new IllegalArgumentException("max must be greater than or equal to 0"); + } + if (max < min) { + throw new IllegalArgumentException("max must be greater than or equal to min"); + } + this.min = min; + this.max = max; + + } + + } + +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/SourceMapFieldFactoryTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/SourceMapFieldFactoryTest.java new file mode 100644 index 0000000000..f1494b2a1f --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/SourceMapFieldFactoryTest.java @@ -0,0 +1,249 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.viewer.field; + +import static org.junit.Assert.*; + +import javax.swing.SwingUtilities; + +import org.junit.*; + +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +import ghidra.framework.options.Options; +import ghidra.framework.store.LockException; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.database.sourcemap.SourceFile; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressOverflowException; +import ghidra.program.model.listing.*; +import ghidra.program.model.sourcemap.SourceFileManager; +import ghidra.program.model.sourcemap.SourceMapEntry; +import ghidra.program.util.SourceMapFieldLocation; +import ghidra.test.*; + +public class SourceMapFieldFactoryTest extends AbstractGhidraHeadedIntegrationTest { + + private TestEnv env; + private ProgramBuilder builder; + private Program program; + private Function entry; + private Address entryPoint; + private CodeBrowserPlugin cb; + private Options fieldOptions; + private SourceFileManager sourceManager; + private static final String SOURCE1_NAME = "test1.c"; + private static final String SOURCE1_PATH = "/dir1/" + SOURCE1_NAME; + private static final String SOURCE2_NAME = "test2.c"; + private static final String SOURCE2_PATH = "/dir2/dir2/" + SOURCE2_NAME; + private SourceFile source1; + private SourceFile source2; + private static final int ROW = 0; + private static final int COLUMN = 0; + + @Before + public void setUp() throws Exception { + init(); + entryPoint = program.getAddressFactory().getDefaultAddressSpace().getAddress(0x1006420); + entry = program.getFunctionManager().getFunctionAt(entryPoint); + env = new TestEnv(); + env.launchDefaultTool(program); + cb = env.getPlugin(CodeBrowserPlugin.class); + SourceMapFieldFactory factory = new SourceMapFieldFactory(); + runSwing(() -> cb.getFormatManager().getCodeUnitFormat().addFactory(factory, ROW, COLUMN)); + fieldOptions = cb.getFormatManager().getFieldOptions(); + } + + @After + public void tearDown() throws Exception { + env.dispose(); + } + + @Test + public void testNoSourceInfo() throws Exception { + assertNotNull(entry); + setBooleanOption(SourceMapFieldFactory.SHOW_INFO_AT_ALL_ADDRESSES_OPTION_NAME, false); + assertFalse(cb.goToField(entryPoint, SourceMapFieldFactory.FIELD_NAME, ROW, COLUMN)); + setBooleanOption(SourceMapFieldFactory.SHOW_INFO_AT_ALL_ADDRESSES_OPTION_NAME, true); + ListingTextField textField = getTextField(entryPoint); + assertEquals(1, textField.getNumRows()); + assertEquals(SourceMapFieldFactory.NO_SOURCE_INFO, textField.getText()); + + } + + @Test + public void testShowFilename() throws Exception { + int txID = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, entryPoint, 2); + } + finally { + program.endTransaction(txID, true); + } + setBooleanOption(SourceMapFieldFactory.SHOW_FILENAME_ONLY_OPTION_NAME, false); + ListingTextField textField = getTextField(entryPoint); + assertEquals(1, textField.getNumRows()); + assertEquals(SOURCE1_PATH + ":1 (2)", textField.getText()); + + setBooleanOption(SourceMapFieldFactory.SHOW_FILENAME_ONLY_OPTION_NAME, true); + textField = getTextField(entryPoint); + assertEquals(1, textField.getNumRows()); + assertEquals(SOURCE1_NAME + ":1 (2)", textField.getText()); + } + + @Test + public void testShowIdentifier() throws Exception { + int txID = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, entryPoint, 2); + } + finally { + program.endTransaction(txID, true); + } + setBooleanOption(SourceMapFieldFactory.SHOW_IDENTIFIER_OPTION_NAME, false); + ListingTextField textField = getTextField(entryPoint); + assertEquals(1, textField.getNumRows()); + assertEquals(SOURCE1_NAME + ":1 (2)", textField.getText()); + + setBooleanOption(SourceMapFieldFactory.SHOW_IDENTIFIER_OPTION_NAME, true); + textField = getTextField(entryPoint); + assertEquals(1, textField.getNumRows()); + assertEquals(SOURCE1_NAME + ":1 (2) [no id]", textField.getText()); + } + + @Test + public void testShowInfoAtAllAddresses() throws Exception { + setBooleanOption(SourceMapFieldFactory.SHOW_FILENAME_ONLY_OPTION_NAME, false); + + Address addr = entryPoint.next(); + Instruction inst = program.getListing().getInstructionAt(addr); + assertEquals(2, inst.getLength()); + Instruction testInst = inst.getNext(); + Address testAddr = testInst.getAddress(); + + int txID = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, addr, 5); + } + finally { + program.endTransaction(txID, true); + } + setBooleanOption(SourceMapFieldFactory.SHOW_INFO_AT_ALL_ADDRESSES_OPTION_NAME, true); + ListingTextField textField = getTextField(testAddr); + assertEquals(1, textField.getNumRows()); + assertEquals(SOURCE1_PATH + ":1 (5)", textField.getText()); + + setBooleanOption(SourceMapFieldFactory.SHOW_INFO_AT_ALL_ADDRESSES_OPTION_NAME, false); + assertFalse(cb.goToField(testAddr, SourceMapFieldFactory.FIELD_NAME, ROW, COLUMN)); + } + + @Test + public void testOffcutSourceMapEntries() throws Exception { + setBooleanOption(SourceMapFieldFactory.SHOW_FILENAME_ONLY_OPTION_NAME, false); + Address addr = entryPoint.next(); + Instruction inst = program.getListing().getInstructionAt(addr); + assertEquals(2, inst.getLength()); + Address testAddr = inst.getAddress().next(); + int txID = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, testAddr, 1); + } + finally { + program.endTransaction(txID, true); + } + + setBooleanOption(SourceMapFieldFactory.SHOW_INFO_AT_ALL_ADDRESSES_OPTION_NAME, true); + ListingTextField textField = getTextField(inst.getAddress()); + assertEquals(1, textField.getNumRows()); + assertEquals(SOURCE1_PATH + ":1 (1)", textField.getText()); + assertEquals(SourceMapFieldFactory.OFFCUT_COLOR, + textField.getFieldElement(0, 0).getColor(0)); + } + + @Test + public void testMaxNumEntries() throws Exception { + + int txID = program.startTransaction("adding source map entries"); + try { + sourceManager.addSourceMapEntry(source1, 1, entryPoint, 1); + sourceManager.addSourceMapEntry(source2, 2, entryPoint, 1); + } + finally { + program.endTransaction(txID, true); + } + ListingTextField textField = getTextField(entryPoint); + assertEquals(2, textField.getNumRows()); + + SwingUtilities.invokeAndWait(() -> fieldOptions + .setInt(SourceMapFieldFactory.MAX_ENTRIES_PER_ADDRESS_OPTION_NAME, 1)); + waitForSwing(); + cb.updateNow(); + + textField = getTextField(entryPoint); + assertEquals(1, textField.getNumRows()); + } + + @Test + public void testFieldLocationSourceMapEntry() throws AddressOverflowException, LockException { + int txID = program.startTransaction("adding source map entries"); + SourceMapEntry entry1 = null; + SourceMapEntry entry2 = null; + try { + entry1 = sourceManager.addSourceMapEntry(source1, 1, entryPoint, 2); + entry2 = sourceManager.addSourceMapEntry(source2, 3, entryPoint, 2); + } + finally { + program.endTransaction(txID, true); + } + ListingTextField textField = getTextField(entryPoint); + FieldFactory fieldFactory = textField.getFieldFactory(); + SourceMapFieldLocation one = + (SourceMapFieldLocation) fieldFactory.getProgramLocation(0, 0, textField); + assertEquals(entry1, one.getSourceMapEntry()); + + SourceMapFieldLocation two = + (SourceMapFieldLocation) fieldFactory.getProgramLocation(1, 0, textField); + assertEquals(entry2, two.getSourceMapEntry()); + } + + private void init() throws Exception { + builder = new ClassicSampleX86ProgramBuilder(); + program = builder.getProgram(); + sourceManager = program.getSourceFileManager(); + int txId = program.startTransaction("adding source files"); + source1 = new SourceFile(SOURCE1_PATH); + source2 = new SourceFile(SOURCE2_PATH); + try { + assertTrue(sourceManager.addSourceFile(source1)); + assertTrue(sourceManager.addSourceFile(source2)); + } + finally { + program.endTransaction(txId, true); + } + } + + private void setBooleanOption(final String name, boolean value) throws Exception { + SwingUtilities.invokeAndWait(() -> fieldOptions.setBoolean(name, value)); + waitForSwing(); + cb.updateNow(); + } + + private ListingTextField getTextField(Address address) { + assertTrue(cb.goToField(address, SourceMapFieldFactory.FIELD_NAME, ROW, COLUMN)); + ListingTextField tf = (ListingTextField) cb.getCurrentField(); + return tf; + } + +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/program/util/SourceMapDiffTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/program/util/SourceMapDiffTest.java new file mode 100644 index 0000000000..2c1716599c --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/program/util/SourceMapDiffTest.java @@ -0,0 +1,496 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.util; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.*; + +import ghidra.program.database.sourcemap.SourceFile; +import ghidra.program.database.sourcemap.SourceFileIdType; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.MemoryAccessException; +import ghidra.program.model.sourcemap.SourceFileManager; +import ghidra.program.model.sourcemap.SourceMapEntry; +import ghidra.test.ClassicSampleX86ProgramBuilder; +import ghidra.util.SourceFileUtils; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class SourceMapDiffTest extends AbstractProgramDiffTest { + + private static final String COMMON_FILE = "/common"; + + // differing files will have the same path but different identifiers + private static final String DIFFERENT = "/different"; + private static final String TEST_FUNC_ADDR_STRING = "0x01002239"; + + private SourceFile common; + private SourceFile p1Only; + private SourceFile p2Only; + + private SourceFileManager p1Manager; + private SourceFileManager p2Manager; + + private List p1Insts; + private List p2Insts; + + private AddressSet p1InstBodies; + private AddressSet p1DiffInsts; + + @Before + public void setUp() throws Exception { + programBuilder1 = new ClassicSampleX86ProgramBuilder(false); + programBuilder2 = new ClassicSampleX86ProgramBuilder(false); + + p1 = programBuilder1.getProgram(); + p2 = programBuilder2.getProgram(); + + p1Insts = new ArrayList<>(); + p2Insts = new ArrayList<>(); + + p1Manager = p1.getSourceFileManager(); + p2Manager = p2.getSourceFileManager(); + + p1InstBodies = new AddressSet(); + p1DiffInsts = new AddressSet(); + + int p1_txID = p1.startTransaction("setup"); + int p2_txID = p2.startTransaction("setup"); + + common = new SourceFile(COMMON_FILE); + byte[] p1Val = SourceFileUtils.longToByteArray(0x11111111); + p1Only = + new SourceFile(DIFFERENT, SourceFileIdType.TIMESTAMP_64, p1Val); + byte[] p2Val = SourceFileUtils.longToByteArray(0x22222222); + p2Only = + new SourceFile(DIFFERENT, SourceFileIdType.TIMESTAMP_64, p2Val); + + try { + p1Manager.addSourceFile(common); + p1Manager.addSourceFile(p1Only); + + p2Manager.addSourceFile(common); + p2Manager.addSourceFile(p2Only); + Address prog1start = p1.getFunctionManager() + .getFunctionAt(p1.getAddressFactory().getAddress(TEST_FUNC_ADDR_STRING)) + .getEntryPoint(); + InstructionIterator p1Iter = p1.getListing().getInstructions(prog1start, true); + + Address prog2start = p2.getFunctionManager() + .getFunctionAt(p2.getAddressFactory().getAddress(TEST_FUNC_ADDR_STRING)) + .getEntryPoint(); + InstructionIterator p2Iter = p2.getListing().getInstructions(prog2start, true); + /** + * 0) no entries + * 1) p1 yes, p2 no + * 2) no entries + * 3) p1 no, p2 yes + * 4) no entries + * 5) files and lines different + * 6) no entries + * 7) files different, lines not + * 8) no entries + * 9) files agree, lines different + * 10) no entries + * 11) files and lines agree + * 12) no entries + * 13) p1 two entries two files, p2 one of them + * 14) no entries + * 15) p2 two entries two files, p1 one of them + * 16) no entries + * 17) p1 two entries one file, p2 two entries one file, one line number diff + * 18) no entries + * 19) length difference + * 20) no entries + * 21) length 0 entry in p1, nothing in p2 + * 22) no entries + * 23) nothing in p1, length 0 entry in p2 + * 24) no entries + * 25) equal length 0 entries + * 26) no entries + * + */ + // 0 + Instruction inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 1 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1DiffInsts.add(inst.getAddress()); + p1Manager.addSourceMapEntry(common, 1, getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 2 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 3 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1DiffInsts.add(inst.getAddress()); + p1Insts.add(inst); + inst = p2Iter.next(); + p2Manager.addSourceMapEntry(common, 3, getBody(inst)); + p2Insts.add(inst); + + // 4 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 5 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1DiffInsts.add(inst.getAddress()); + p1Manager.addSourceMapEntry(p1Only, 51, getBody(inst)); + p1Insts.add(inst); + inst = p2Iter.next(); + p2Manager.addSourceMapEntry(p2Only, 52, getBody(inst)); + p2Insts.add(inst); + + // 6 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 7 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1DiffInsts.add(inst.getAddress()); + p1Manager.addSourceMapEntry(p1Only, 7, getBody(inst)); + p1Insts.add(inst); + inst = p2Iter.next(); + p2Manager.addSourceMapEntry(p2Only, 7, getBody(inst)); + p2Insts.add(inst); + + // 8 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 9 + // XOR EAX,EAX length 2 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1DiffInsts.add(inst.getAddress()); + p1Manager.addSourceMapEntry(common, 91, getBody(inst)); + p1Insts.add(inst); + inst = p2Iter.next(); + p2Manager.addSourceMapEntry(common, 92, getBody(inst)); + p2Insts.add(inst); + + // 10 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 11 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + // files,lines, and lengths agree - not a diff + p1Manager.addSourceMapEntry(common, 11, getBody(inst)); + p1Insts.add(inst); + inst = p2Iter.next(); + p2Manager.addSourceMapEntry(common, 11, getBody(inst)); + p2Insts.add(inst); + + // 12 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 13 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p1DiffInsts.add(inst.getAddress()); + p1Manager.addSourceMapEntry(common, 13, getBody(inst)); + p1Manager.addSourceMapEntry(p1Only, 13, getBody(inst)); + inst = p2Iter.next(); + p2Insts.add(inst); + p2Manager.addSourceMapEntry(common, 13, getBody(inst)); + + // 14 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 15 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p1DiffInsts.add(inst.getAddress()); + p1Manager.addSourceMapEntry(common, 15, getBody(inst)); + inst = p2Iter.next(); + p2Insts.add(inst); + p2Manager.addSourceMapEntry(common, 15, getBody(inst)); + p2Manager.addSourceMapEntry(p2Only, 15, getBody(inst)); + + // 16 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 17 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p1DiffInsts.add(inst.getAddress()); + p1Manager.addSourceMapEntry(common, 17, getBody(inst)); + p1Manager.addSourceMapEntry(common, 18, getBody(inst)); + inst = p2Iter.next(); + p2Insts.add(inst); + p2Manager.addSourceMapEntry(common, 17, getBody(inst)); + p1Manager.addSourceMapEntry(common, 19, getBody(inst)); + + // 18 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 19 + inst = p1Iter.next(); + // length of this instruction is 2 + p1InstBodies.add(getBody(inst)); + p1Manager.addSourceMapEntry(common, 1000, inst.getAddress(), 1); + p1Insts.add(inst); + p1DiffInsts.add(inst.getAddress()); + inst = p2Iter.next(); + p2Manager.addSourceMapEntry(common, 1000, inst.getAddress(), 2); + p2Insts.add(inst); + + // 20 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 21 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1DiffInsts.add(inst.getAddress()); + p1Manager.addSourceMapEntry(p1Only, 1, inst.getAddress(), 0); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 22 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 23 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1DiffInsts.add(inst.getAddress()); + p1Insts.add(inst); + inst = p2Iter.next(); + p2Manager.addSourceMapEntry(p2Only, 3, inst.getAddress(), 0); + p2Insts.add(inst); + + // 24 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + // 25 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + // files,lines, and lengths agree - not a diff + p1Manager.addSourceMapEntry(common, 25, inst.getAddress(), 0); + p1Insts.add(inst); + inst = p2Iter.next(); + p2Manager.addSourceMapEntry(common, 25, inst.getAddress(), 0); + p2Insts.add(inst); + + // 26 + inst = p1Iter.next(); + p1InstBodies.add(getBody(inst)); + p1Insts.add(inst); + p2Insts.add(p2Iter.next()); + + } + + finally { + p1.endTransaction(p1_txID, true); + p2.endTransaction(p2_txID, true); + } + } + + @After + public void tearDown() throws Exception { + if (programBuilder1 != null) { + programBuilder1.dispose(); + } + if (programBuilder2 != null) { + programBuilder2.dispose(); + } + } + + @Test + public void simpleDiffTest() + throws ProgramConflictException, IllegalArgumentException, CancelledException { + assertEquals(p1Insts.size(), p2Insts.size()); + AddressSet testSet = new AddressSet(); + p1Insts.forEach(i -> testSet.add(getBody(i))); + assertEquals(testSet, p1InstBodies); + programDiff = new ProgramDiff(p1, p2, p1InstBodies); + programDiff.setFilter(new ProgramDiffFilter(ProgramDiffFilter.SOURCE_MAP_DIFFS)); + // verify that the differences found by progamDiff.getDifferences are exactly + // the differences created in the setUp method + assertEquals(p1DiffInsts, + programDiff.getDifferences(programDiff.getFilter(), TaskMonitor.DUMMY)); + } + + @Test + public void testReplace() + throws ProgramConflictException, MemoryAccessException, CancelledException { + ProgramMergeManager programMerge = + new ProgramMergeManager(p1, p2, p1InstBodies); + ProgramMergeFilter mergeFilter = + new ProgramMergeFilter(ProgramMergeFilter.SOURCE_MAP, ProgramMergeFilter.REPLACE); + + // for good measure, verify one of the differences before merge + SourceMapEntry p1Info = p1Manager.getSourceMapEntries(p1Insts.get(5).getAddress()).get(0); + SourceMapEntry p2Info = p2Manager.getSourceMapEntries(p2Insts.get(5).getAddress()).get(0); + + assertNotEquals(p1Info.getSourceFile(), p2Info.getSourceFile()); + assertNotEquals(p1Info.getLineNumber(), p2Info.getLineNumber()); + + int txId = p1.startTransaction("merging"); + try { + boolean success = programMerge.merge(p1InstBodies, mergeFilter, TaskMonitor.DUMMY); + assertTrue(success); + } + finally { + p1.endTransaction(txId, true); + } + + // now verify that source map info is the same for all addresses (not just beginnings of + // instructions) + AddressSet p2InstBodies = new AddressSet(); + p2Insts.forEach(i -> p2InstBodies.add(getBody(i))); + assertEquals(p1InstBodies.getNumAddresses(), p2InstBodies.getNumAddresses()); + AddressIterator p1Iter = p1InstBodies.getAddresses(true); + AddressIterator p2Iter = p2InstBodies.getAddresses(true); + while (p1Iter.hasNext()) { + Address p1Addr = p1Iter.next(); + Address p2Addr = p2Iter.next(); + List p1Entries = p1Manager.getSourceMapEntries(p1Addr); + List p2Entries = p2Manager.getSourceMapEntries(p2Addr); + assertEquals(p1Entries.size(), p2Entries.size()); + for (SourceMapEntry p1Entry : p1Entries) { + int index = Collections.binarySearch(p2Entries, p1Entry); + assertTrue(index >= 0); + } + } + } + + @Test + public void testIgnoreFilter() + throws ProgramConflictException, MemoryAccessException, CancelledException { + ProgramMergeManager programMerge = + new ProgramMergeManager(p1, p2, p1InstBodies); + ProgramMergeFilter mergeFilter = + new ProgramMergeFilter(ProgramMergeFilter.SOURCE_MAP, ProgramMergeFilter.IGNORE); + + // verify one of the differences before merge + SourceMapEntry p1Info = p1Manager.getSourceMapEntries(p1Insts.get(5).getAddress()).get(0); + SourceMapEntry p2Info = p2Manager.getSourceMapEntries(p2Insts.get(5).getAddress()).get(0); + + assertNotEquals(p1Info.getSourceFile(), p2Info.getSourceFile()); + assertNotEquals(p1Info.getLineNumber(), p2Info.getLineNumber()); + + int txId = p1.startTransaction("merging"); + try { + boolean success = programMerge.merge(p1InstBodies, mergeFilter, TaskMonitor.DUMMY); + assertTrue(success); + } + finally { + p1.endTransaction(txId, true); + } + + // verify the difference is still there + p1Info = p1Manager.getSourceMapEntries(p1Insts.get(5).getAddress()).get(0); + p2Info = p2Manager.getSourceMapEntries(p2Insts.get(5).getAddress()).get(0); + + assertNotEquals(p1Info.getSourceFile(), p2Info.getSourceFile()); + assertNotEquals(p1Info.getLineNumber(), p2Info.getLineNumber()); + + } + + @Test + public void testIgnoreOverlappingEntriesInDiff() + throws ProgramConflictException, IllegalArgumentException, CancelledException { + Address p1Inst9Addr = p1.getAddressFactory().getDefaultAddressSpace().getAddress(0x1002257); + Instruction p1Inst9 = p1.getListing().getInstructionAt(p1Inst9Addr); + assertNotNull(p1Inst9); + assertEquals(2, p1Inst9.getLength()); + assertEquals("XOR EAX,EAX", p1Inst9.toString()); + + AddressSet testSet = new AddressSet(); + testSet.add(getBody(p1Inst9)); + programDiff = new ProgramDiff(p1, p2, testSet); + programDiff.setFilter(new ProgramDiffFilter(ProgramDiffFilter.SOURCE_MAP_DIFFS)); + //verify that there is a difference at p1Inst9Addr + assertEquals(new AddressSet(p1Inst9Addr), + programDiff.getDifferences(programDiff.getFilter(), TaskMonitor.DUMMY)); + + testSet.clear(); + testSet.add(p1Inst9Addr.add(1)); + + programDiff = new ProgramDiff(p1, p2, testSet); + programDiff.setFilter(new ProgramDiffFilter(ProgramDiffFilter.SOURCE_MAP_DIFFS)); + //verify that no differences are reported, since the beginning of the source map entry + //is not in testSet + assertTrue( + programDiff.getDifferences(programDiff.getFilter(), TaskMonitor.DUMMY).isEmpty()); + } + + @Test + public void testMergeFilterChanged() { + ProgramMergeFilter mergeFilter = + new ProgramMergeFilter(ProgramMergeFilter.SOURCE_MAP, ProgramMergeFilter.MERGE); + // MERGE not valid for source files - should be changed to REPLACE by the ProgramMergeFilter + // constructor + assertEquals(ProgramMergeFilter.REPLACE, + mergeFilter.getFilter(ProgramMergeFilter.SOURCE_MAP)); + } + + private AddressRange getBody(CodeUnit cu) { + return new AddressRangeImpl(cu.getMinAddress(), cu.getMaxAddress()); + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/ApplyLineNumbers.java b/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/ApplyLineNumbers.java index 514594a166..d5b4b2184a 100644 --- a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/ApplyLineNumbers.java +++ b/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/ApplyLineNumbers.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,12 +15,16 @@ */ package ghidra.app.util.bin.format.pdb; -import java.io.File; - import ghidra.app.util.importer.MessageLog; +import ghidra.framework.store.LockException; +import ghidra.program.database.sourcemap.SourceFile; import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressOverflowException; import ghidra.program.model.listing.CodeUnit; import ghidra.program.model.listing.Program; +import ghidra.program.model.sourcemap.SourceFileManager; +import ghidra.util.Msg; +import ghidra.util.SourceFileUtils; import ghidra.util.task.TaskMonitor; import ghidra.util.xml.XmlUtilities; import ghidra.xml.XmlElement; @@ -38,6 +42,12 @@ class ApplyLineNumbers { } void applyTo(TaskMonitor monitor, MessageLog log) { + if (!program.hasExclusiveAccess()) { + Msg.showWarn(this, null, "Cannot Apply SourceMap Information", + "Exclusive access to the program is required to apply source map information"); + return; + } + SourceFileManager manager = program.getSourceFileManager(); while (xmlParser.hasNext()) { if (monitor.isCancelled()) { return; @@ -47,11 +57,16 @@ class ApplyLineNumbers { break; } elem = xmlParser.next();//line number start tag - String sourcefileName = elem.getAttribute("source_file"); + String sourceFilePath = elem.getAttribute("source_file"); int start = XmlUtilities.parseInt(elem.getAttribute("start")); int addr = XmlUtilities.parseInt(elem.getAttribute("addr")); Address address = PdbUtil.reladdr(program, addr); + long length = 0; + String lengthElem = elem.getAttribute("length"); + if (lengthElem != null) { + length = XmlUtilities.parseLong(lengthElem); + } // The following line was changed from getCodeUnitAt(address) to // getCodeUnitContaining(address) in order to fix an issue where the PDB associates // a line number with base part of an instruction instead of the prefix part of an @@ -63,16 +78,29 @@ class ApplyLineNumbers { CodeUnit cu = program.getListing().getCodeUnitContaining(address); if (cu == null) { log.appendMsg("PDB", - "Could not apply source code line number (no code unit found at " + address + - ")"); + "Skipping source map info (no code unit found at " + address + ")"); + continue; } - else { - cu.setProperty("Source Path", sourcefileName); - cu.setProperty("Source File", new File(sourcefileName).getName()); - cu.setProperty("Source Line", start); + + try { + SourceFile sourceFile = + SourceFileUtils.getSourceFileFromPathString(sourceFilePath); + manager.addSourceFile(sourceFile); + manager.addSourceMapEntry(sourceFile, start, address, length); } - //String comment = sourcefile.getName()+":"+start; - //setComment(CodeUnit.PRE_COMMENT, program.getListing(), address, comment); + catch (LockException e) { + throw new AssertionError("LockException after exclusive access verified!"); + } + catch (AddressOverflowException e) { + log.appendMsg("PDB", "AddressOverflow for source map info: %s, %d, %s, %d" + .formatted(sourceFilePath, start, address.toString(), length)); + } + catch (IllegalArgumentException e) { + // thrown by SourceFileManager.addSourceMapEntry if the new entry conflicts + // with an existing entry + log.appendMsg("PDB", e.getMessage()); + } + elem = xmlParser.next();//line number end tag } } diff --git a/Ghidra/Features/PDB/src/pdb/cpp/iterate.cpp b/Ghidra/Features/PDB/src/pdb/cpp/iterate.cpp index 9ab81b8a07..e2991297a2 100644 --- a/Ghidra/Features/PDB/src/pdb/cpp/iterate.cpp +++ b/Ghidra/Features/PDB/src/pdb/cpp/iterate.cpp @@ -322,9 +322,11 @@ void dumpFunctionLines( IDiaSymbol& symbol, IDiaSession& session ) pLine->get_lineNumber( &start ); DWORD end = 0; pLine->get_lineNumberEnd( &end ); + DWORD range_length = 0; + pLine->get_length( &range_length ); - printf("%S \n", - indent(12).c_str(), escapeXmlEntities(wsSourceFileName).c_str(), start, end, addr); + printf("%S \n", + indent(12).c_str(), escapeXmlEntities(wsSourceFileName).c_str(), start, end, addr, range_length); } } diff --git a/Ghidra/Features/ProgramDiff/src/main/help/help/topics/Diff/Diff.htm b/Ghidra/Features/ProgramDiff/src/main/help/help/topics/Diff/Diff.htm index d3062dd82e..274f22e813 100644 --- a/Ghidra/Features/ProgramDiff/src/main/help/help/topics/Diff/Diff.htm +++ b/Ghidra/Features/ProgramDiff/src/main/help/help/topics/Diff/Diff.htm @@ -462,6 +462,9 @@
  • Function tags differ.
  • +

    Source Map + - detect any addresses where the source map information is different. +

    When the Determine Program Differences dialog is initially displayed, all the Differences check boxes are checked. This indicates @@ -1305,6 +1308,15 @@ Can be: Ignore, Replace, or Merge.
    + + + Source Map
    + + + Controls whether Source Map differences will be applied. + Can be: Ignore (the default) or Replace.
    + + diff --git a/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/DiffApplySettingsOptionManager.java b/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/DiffApplySettingsOptionManager.java index 5d492e2f63..0f0d1231a8 100644 --- a/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/DiffApplySettingsOptionManager.java +++ b/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/DiffApplySettingsOptionManager.java @@ -43,35 +43,38 @@ class DiffApplySettingsOptionManager { private static final int PROPERTIES = 1 << 11; private static final int FUNCTIONS = 1 << 12; private static final int FUNCTION_TAGS = 1 << 13; + private static final int SOURCE_MAP = 1 << 14; - private static final String OPTION_PROGRAM_CONTEXT = DIFF_APPLY_SETTINGS_OPTIONS + - Options.DELIMITER + "Program Context"; - private static final String OPTION_BYTES = DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + - "Bytes"; - private static final String OPTION_CODE_UNITS = DIFF_APPLY_SETTINGS_OPTIONS + - Options.DELIMITER + "Code Units"; - private static final String OPTION_REFERENCES = DIFF_APPLY_SETTINGS_OPTIONS + - Options.DELIMITER + "References"; - private static final String OPTION_PLATE_COMMENTS = DIFF_APPLY_SETTINGS_OPTIONS + - Options.DELIMITER + "Plate Comments"; - private static final String OPTION_PRE_COMMENTS = DIFF_APPLY_SETTINGS_OPTIONS + - Options.DELIMITER + "Pre Comments"; - private static final String OPTION_EOL_COMMENTS = DIFF_APPLY_SETTINGS_OPTIONS + - Options.DELIMITER + "End Of Line Comments"; - private static final String OPTION_REPEATABLE_COMMENTS = DIFF_APPLY_SETTINGS_OPTIONS + - Options.DELIMITER + "Repeatable Comments"; - private static final String OPTION_POST_COMMENTS = DIFF_APPLY_SETTINGS_OPTIONS + - Options.DELIMITER + "Post Comments"; - private static final String OPTION_SYMBOLS = DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + - "Labels"; - private static final String OPTION_BOOKMARKS = DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + - "Bookmarks"; - private static final String OPTION_PROPERTIES = DIFF_APPLY_SETTINGS_OPTIONS + - Options.DELIMITER + "Properties"; - private static final String OPTION_FUNCTIONS = DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + - "Functions"; + private static final String OPTION_PROGRAM_CONTEXT = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Program Context"; + private static final String OPTION_BYTES = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Bytes"; + private static final String OPTION_CODE_UNITS = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Code Units"; + private static final String OPTION_REFERENCES = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "References"; + private static final String OPTION_PLATE_COMMENTS = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Plate Comments"; + private static final String OPTION_PRE_COMMENTS = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Pre Comments"; + private static final String OPTION_EOL_COMMENTS = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "End Of Line Comments"; + private static final String OPTION_REPEATABLE_COMMENTS = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Repeatable Comments"; + private static final String OPTION_POST_COMMENTS = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Post Comments"; + private static final String OPTION_SYMBOLS = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Labels"; + private static final String OPTION_BOOKMARKS = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Bookmarks"; + private static final String OPTION_PROPERTIES = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Properties"; + private static final String OPTION_FUNCTIONS = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Functions"; private static final String OPTION_FUNCTION_TAGS = DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Function Tags"; + private static final String OPTION_SOURCE_MAP = + DIFF_APPLY_SETTINGS_OPTIONS + Options.DELIMITER + "Source Map"; // public static final String MERGE = "Merge"; // public static final String MERGE_SYMBOLS_1 = "Merge"; @@ -79,6 +82,7 @@ class DiffApplySettingsOptionManager { public static enum REPLACE_CHOICE { IGNORE("Ignore"), REPLACE("Replace"); + private String description; REPLACE_CHOICE(String description) { @@ -93,6 +97,7 @@ class DiffApplySettingsOptionManager { public static enum MERGE_CHOICE { IGNORE("Ignore"), REPLACE("Replace"), MERGE("Merge"); + private String description; MERGE_CHOICE(String description) { @@ -111,6 +116,7 @@ class DiffApplySettingsOptionManager { REPLACE("Replace"), MERGE_DONT_SET_PRIMARY("Merge"), MERGE_AND_SET_PRIMARY("Merge & Set Primary"); + private String description; SYMBOL_MERGE_CHOICE(String description) { @@ -146,10 +152,7 @@ class DiffApplySettingsOptionManager { options.setOptionsHelpLocation(help); // Set the help strings - options.registerOption( - OPTION_PROGRAM_CONTEXT, - REPLACE_CHOICE.REPLACE, - help, + options.registerOption(OPTION_PROGRAM_CONTEXT, REPLACE_CHOICE.REPLACE, help, getReplaceDescription("program context register value", "program context register values")); options.registerOption(OPTION_BYTES, REPLACE_CHOICE.REPLACE, help, @@ -180,6 +183,8 @@ class DiffApplySettingsOptionManager { getReplaceDescription("function", "functions")); options.registerOption(OPTION_FUNCTION_TAGS, MERGE_CHOICE.MERGE, help, getReplaceDescription("function tag", "function tags")); + options.registerOption(OPTION_SOURCE_MAP, REPLACE_CHOICE.IGNORE, help, + getReplaceDescription("source map", "source map")); getDefaultApplyFilter(); } @@ -218,6 +223,7 @@ class DiffApplySettingsOptionManager { REPLACE_CHOICE properties = options.getEnum(OPTION_PROPERTIES, REPLACE_CHOICE.REPLACE); REPLACE_CHOICE functions = options.getEnum(OPTION_FUNCTIONS, REPLACE_CHOICE.REPLACE); MERGE_CHOICE functionTags = options.getEnum(OPTION_FUNCTION_TAGS, MERGE_CHOICE.MERGE); + REPLACE_CHOICE sourceMap = options.getEnum(OPTION_SOURCE_MAP, REPLACE_CHOICE.IGNORE); // Convert the options to a merge filter. ProgramMergeFilter filter = new ProgramMergeFilter(); @@ -238,6 +244,7 @@ class DiffApplySettingsOptionManager { filter.setFilter(ProgramMergeFilter.FUNCTION_TAGS, functionTags.ordinal()); filter.setFilter(ProgramMergeFilter.PRIMARY_SYMBOL, convertSymbolMergeChoiceToReplaceChoiceForPrimay(symbols).ordinal()); + filter.setFilter(ProgramMergeFilter.SOURCE_MAP, sourceMap.ordinal()); return filter; } @@ -293,6 +300,7 @@ class DiffApplySettingsOptionManager { saveReplaceOption(options, newDefaultApplyFilter, BOOKMARKS); saveReplaceOption(options, newDefaultApplyFilter, PROPERTIES); saveReplaceOption(options, newDefaultApplyFilter, FUNCTIONS); + saveReplaceOption(options, newDefaultApplyFilter, SOURCE_MAP); saveMergeOption(options, newDefaultApplyFilter, PLATE_COMMENTS); saveMergeOption(options, newDefaultApplyFilter, PRE_COMMENTS); @@ -311,8 +319,9 @@ class DiffApplySettingsOptionManager { private void saveCodeUnitReplaceOption(Options options, ProgramMergeFilter defaultApplyFilter, int setting) { int filter = - (defaultApplyFilter.getFilter(ProgramMergeFilter.INSTRUCTIONS) >= defaultApplyFilter.getFilter(ProgramMergeFilter.DATA)) ? ProgramMergeFilter.INSTRUCTIONS - : ProgramMergeFilter.DATA; + (defaultApplyFilter.getFilter(ProgramMergeFilter.INSTRUCTIONS) >= defaultApplyFilter + .getFilter(ProgramMergeFilter.DATA)) ? ProgramMergeFilter.INSTRUCTIONS + : ProgramMergeFilter.DATA; REPLACE_CHOICE defaultSetting = REPLACE_CHOICE.REPLACE; REPLACE_CHOICE optionSetting = options.getEnum(getOptionName(setting), defaultSetting); REPLACE_CHOICE diffSetting = convertTypeToReplaceEnum(defaultApplyFilter, filter); @@ -332,7 +341,8 @@ class DiffApplySettingsOptionManager { } } - private void saveMergeOption(Options options, ProgramMergeFilter defaultApplyFilter, int setting) { + private void saveMergeOption(Options options, ProgramMergeFilter defaultApplyFilter, + int setting) { MERGE_CHOICE defaultSetting = MERGE_CHOICE.MERGE; MERGE_CHOICE optionSetting = options.getEnum(getOptionName(setting), defaultSetting); MERGE_CHOICE diffSetting = @@ -507,7 +517,8 @@ class DiffApplySettingsOptionManager { * @param type the ProgramMergeFilter filter type * @return the StringEnum */ - private REPLACE_CHOICE convertTypeToReplaceEnum(ProgramMergeFilter defaultApplyFilter, int type) { + private REPLACE_CHOICE convertTypeToReplaceEnum(ProgramMergeFilter defaultApplyFilter, + int type) { int filter = defaultApplyFilter.getFilter(type); return REPLACE_CHOICE.values()[filter]; } diff --git a/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/DiffApplySettingsProvider.java b/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/DiffApplySettingsProvider.java index 3473c74320..2521916e21 100644 --- a/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/DiffApplySettingsProvider.java +++ b/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/DiffApplySettingsProvider.java @@ -34,6 +34,7 @@ import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.framework.plugintool.Plugin; import ghidra.program.util.ProgramMergeFilter; import ghidra.util.HelpLocation; +import ghidra.util.Msg; /** * The DiffSettingsDialog is used to change the types of differences currently @@ -63,6 +64,7 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { private Choice propertiesCB; private Choice functionsCB; private Choice functionTagsCB; + private Choice sourceMapCB; private int applyProgramContext; private int applyBytes; @@ -78,6 +80,7 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { private int applyProperties; private int applyFunctions; private int applyFunctionTags; + private int applySourceMap; private int replacePrimary; private ProgramMergeFilter applyFilter; @@ -107,8 +110,7 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { public void addActions() { plugin.getTool() - .addLocalAction(this, - new SaveApplySettingsAction(this, plugin.applySettingsMgr)); + .addLocalAction(this, new SaveApplySettingsAction(this, plugin.applySettingsMgr)); plugin.getTool().addLocalAction(this, new DiffIgnoreAllAction(this)); plugin.getTool().addLocalAction(this, new DiffReplaceAllAction(this)); plugin.getTool().addLocalAction(this, new DiffMergeAllAction(this)); @@ -152,7 +154,7 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { }); choices.add(refsCB); - plateCommentsCB = new Choice("Plate Comments", true); + plateCommentsCB = new Choice("Comments, Plate", true); plateCommentsCB.addActionListener(e -> { applyPlateComments = plateCommentsCB.getSelectedIndex(); applyFilter.setFilter(ProgramMergeFilter.PLATE_COMMENTS, applyPlateComments); @@ -160,7 +162,7 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { }); choices.add(plateCommentsCB); - preCommentsCB = new Choice("Pre Comments", true); + preCommentsCB = new Choice("Comments, Pre", true); preCommentsCB.addActionListener(e -> { applyPreComments = preCommentsCB.getSelectedIndex(); applyFilter.setFilter(ProgramMergeFilter.PRE_COMMENTS, applyPreComments); @@ -168,7 +170,7 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { }); choices.add(preCommentsCB); - eolCommentsCB = new Choice("Eol Comments", true); + eolCommentsCB = new Choice("Comments, EOL", true); eolCommentsCB.addActionListener(e -> { applyEolComments = eolCommentsCB.getSelectedIndex(); applyFilter.setFilter(ProgramMergeFilter.EOL_COMMENTS, applyEolComments); @@ -176,7 +178,7 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { }); choices.add(eolCommentsCB); - repeatableCommentsCB = new Choice("Repeatable Comments", true); + repeatableCommentsCB = new Choice("Comments, Repeatable", true); repeatableCommentsCB.addActionListener(e -> { applyRepeatableComments = repeatableCommentsCB.getSelectedIndex(); applyFilter.setFilter(ProgramMergeFilter.REPEATABLE_COMMENTS, applyRepeatableComments); @@ -184,7 +186,7 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { }); choices.add(repeatableCommentsCB); - postCommentsCB = new Choice("Post Comments", true); + postCommentsCB = new Choice("Comments, Post", true); postCommentsCB.addActionListener(e -> { applyPostComments = postCommentsCB.getSelectedIndex(); applyFilter.setFilter(ProgramMergeFilter.POST_COMMENTS, applyPostComments); @@ -240,6 +242,25 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { }); choices.add(functionTagsCB); + sourceMapCB = new Choice("Source Map", false); + if (!plugin.getFirstProgram().hasExclusiveAccess()) { + sourceMapCB.setSelectedIndex(ProgramMergeFilter.IGNORE); + } + sourceMapCB.addActionListener(e -> { + applySourceMap = sourceMapCB.getSelectedIndex(); + if (!plugin.getFirstProgram().hasExclusiveAccess()) { + if (applySourceMap != ProgramMergeFilter.IGNORE) { + Msg.showWarn(this, null, "Exclusive Access Required", + "Exclusive access required to change source map information"); + sourceMapCB.setSelectedIndex(ProgramMergeFilter.IGNORE); + applySourceMap = ProgramMergeFilter.IGNORE; + } + } + applyFilter.setFilter(ProgramMergeFilter.SOURCE_MAP, applySourceMap); + applyFilterChanged(); + }); + choices.add(sourceMapCB); + int maxLabelWidth = 0; int maxComboWidth = 0; for (Choice choice : choices) { @@ -294,6 +315,7 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { propertiesCB.setSelectedIndex(applyProperties); functionsCB.setSelectedIndex(applyFunctions); functionTagsCB.setSelectedIndex(applyFunctionTags); + sourceMapCB.setSelectedIndex(applySourceMap); } finally { adjustingApplyFilter = false; @@ -340,6 +362,7 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { applyFunctions = applyFilter.getFilter(ProgramMergeFilter.FUNCTIONS); applyFunctionTags = applyFilter.getFilter(ProgramMergeFilter.FUNCTION_TAGS); replacePrimary = applyFilter.getFilter(ProgramMergeFilter.PRIMARY_SYMBOL); + applySourceMap = applyFilter.getFilter(ProgramMergeFilter.SOURCE_MAP); adjustApplyFilter(); } @@ -402,11 +425,7 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { new GComboBox<>(allowMerge ? DiffApplySettingsOptionManager.MERGE_CHOICE.values() : DiffApplySettingsOptionManager.REPLACE_CHOICE.values()); applyCB.setName(type + " Diff Apply CB"); - String typeName = type; - if (typeName.endsWith(" Comments")) { - typeName = "Comments, " + typeName.substring(0, typeName.length() - 9); - } - label = new GDLabel(" " + typeName + " "); + label = new GDLabel(" " + type + " "); label.setHorizontalAlignment(SwingConstants.RIGHT); add(applyCB, BorderLayout.EAST); add(label, BorderLayout.CENTER); @@ -438,7 +457,7 @@ public class DiffApplySettingsProvider extends ComponentProviderAdapter { @Override public int compareTo(Choice o) { - return label.toString().compareTo(o.label.toString()); + return type.compareTo(o.type); } } diff --git a/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/ExecuteDiffDialog.java b/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/ExecuteDiffDialog.java index 6cf6559f31..202193c8a7 100644 --- a/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/ExecuteDiffDialog.java +++ b/Ghidra/Features/ProgramDiff/src/main/java/ghidra/app/plugin/core/diff/ExecuteDiffDialog.java @@ -52,6 +52,7 @@ public class ExecuteDiffDialog extends ReusableDialogComponentProvider { private JCheckBox diffBookmarksCB; private JCheckBox diffPropertiesCB; private JCheckBox diffFunctionsCB; + private JCheckBox diffSourceMapCB; private JButton selectAllButton = new JButton("Select All"); private JButton deselectAllButton = new JButton("Deselect All"); private JCheckBox limitToSelectionCB; @@ -66,6 +67,7 @@ public class ExecuteDiffDialog extends ReusableDialogComponentProvider { private boolean diffBookmarks; private boolean diffProperties; private boolean diffFunctions; + private boolean diffSourceMap; private ProgramDiffFilter diffFilter; private JPanel diffPanel; @@ -191,9 +193,8 @@ public class ExecuteDiffDialog extends ReusableDialogComponentProvider { */ private JPanel createDiffFilterPanel() { JPanel checkBoxPanel = new JPanel(); - checkBoxPanel.setToolTipText( - "Check the types of differences between the two " + - "programs that you want detected and highlighted."); + checkBoxPanel.setToolTipText("Check the types of differences between the two " + + "programs that you want detected and highlighted."); createBytesCheckBox(); createLabelsCheckBox(); @@ -204,17 +205,19 @@ public class ExecuteDiffDialog extends ReusableDialogComponentProvider { createBookmarksCheckBox(); createPropertiesCheckBox(); createFunctionsCheckBox(); + createSourceMapCheckBox(); - checkBoxPanel.setLayout(new GridLayout(3, 3, 5, 0)); - checkBoxPanel.add(diffBytesCB); - checkBoxPanel.add(diffLabelsCB); - checkBoxPanel.add(diffCodeUnitsCB); - checkBoxPanel.add(diffReferencesCB); - checkBoxPanel.add(diffProgramContextCB); - checkBoxPanel.add(diffCommentsCB); + checkBoxPanel.setLayout(new GridLayout(2, 5, 5, 0)); checkBoxPanel.add(diffBookmarksCB); - checkBoxPanel.add(diffPropertiesCB); + checkBoxPanel.add(diffBytesCB); + checkBoxPanel.add(diffCodeUnitsCB); + checkBoxPanel.add(diffCommentsCB); checkBoxPanel.add(diffFunctionsCB); + checkBoxPanel.add(diffLabelsCB); + checkBoxPanel.add(diffProgramContextCB); + checkBoxPanel.add(diffPropertiesCB); + checkBoxPanel.add(diffReferencesCB); + checkBoxPanel.add(diffSourceMapCB); JPanel buttonPanel = new JPanel(); createSelectAllButton(); @@ -254,8 +257,8 @@ public class ExecuteDiffDialog extends ReusableDialogComponentProvider { private void createCodeUnitsCheckBox() { diffCodeUnitsCB = new GCheckBox("Code Units", diffCodeUnits); diffCodeUnitsCB.setName("CodeUnitsDiffCB"); - diffCodeUnitsCB.setToolTipText( - "Highlight the instruction, data, " + "and equate differences."); + diffCodeUnitsCB + .setToolTipText("Highlight the instruction, data, " + "and equate differences."); diffCodeUnitsCB.addItemListener(event -> { diffCodeUnits = (event.getStateChange() == ItemEvent.SELECTED); diffFilter.setFilter(ProgramDiffFilter.CODE_UNIT_DIFFS, diffCodeUnits); @@ -334,6 +337,17 @@ public class ExecuteDiffDialog extends ReusableDialogComponentProvider { }); } + private void createSourceMapCheckBox() { + diffSourceMapCB = new GCheckBox("Source Map", diffSourceMap); + diffSourceMapCB.setName("SourceMapDiffCB"); + diffSourceMapCB.setToolTipText("Highlight Source Map Differences"); + diffSourceMapCB.addItemListener(event -> { + diffSourceMap = (event.getStateChange() == ItemEvent.SELECTED); + diffFilter.setFilter(ProgramDiffFilter.SOURCE_MAP_DIFFS, diffSourceMap); + clearStatusText(); + }); + } + private void createSelectAllButton() { selectAllButton.addActionListener(e -> setSelectAll(true)); selectAllButton.setMnemonic('S'); @@ -354,6 +368,7 @@ public class ExecuteDiffDialog extends ReusableDialogComponentProvider { diffBookmarksCB.setSelected(selected); diffPropertiesCB.setSelected(selected); diffFunctionsCB.setSelected(selected); + diffSourceMapCB.setSelected(selected); } private void adjustDiffFilter() { @@ -366,7 +381,7 @@ public class ExecuteDiffDialog extends ReusableDialogComponentProvider { diffBookmarksCB.setSelected(diffBookmarks); diffPropertiesCB.setSelected(diffProperties); diffFunctionsCB.setSelected(diffFunctions); - + diffSourceMapCB.setSelected(diffSourceMap); } void setPgmContextEnabled(boolean enable) { @@ -402,6 +417,7 @@ public class ExecuteDiffDialog extends ReusableDialogComponentProvider { diffBookmarks = diffFilter.getFilter(ProgramDiffFilter.BOOKMARK_DIFFS); diffProperties = diffFilter.getFilter(ProgramDiffFilter.USER_DEFINED_DIFFS); diffFunctions = diffFilter.getFilter(ProgramDiffFilter.FUNCTION_DIFFS); + diffSourceMap = diffFilter.getFilter(ProgramDiffFilter.SOURCE_MAP_DIFFS); adjustDiffFilter(); } @@ -469,7 +485,7 @@ public class ExecuteDiffDialog extends ReusableDialogComponentProvider { */ boolean hasDiffSelection() { return (diffBytes || diffLabels || diffCodeUnits || diffProgramContext || diffReferences || - diffComments || diffBookmarks || diffProperties || diffFunctions); + diffComments || diffBookmarks || diffProperties || diffFunctions || diffSourceMap); } /** @@ -478,7 +494,7 @@ public class ExecuteDiffDialog extends ReusableDialogComponentProvider { boolean isMarkingAllDiffs() { return (diffBytes && diffLabels && diffCodeUnits && ((!pgmContextEnabled) || diffProgramContext) && diffReferences && diffComments && - diffBookmarks && diffProperties && diffFunctions); + diffBookmarks && diffProperties && diffFunctions && diffSourceMap); } } diff --git a/Ghidra/Framework/DB/src/main/java/db/RecordIterator.java b/Ghidra/Framework/DB/src/main/java/db/RecordIterator.java index 45550f30b7..ad20faccd0 100644 --- a/Ghidra/Framework/DB/src/main/java/db/RecordIterator.java +++ b/Ghidra/Framework/DB/src/main/java/db/RecordIterator.java @@ -22,31 +22,31 @@ import java.io.IOException; * data records within a table. */ public interface RecordIterator { - + /** * Return true if a Record is available in the forward direction. * @throws IOException thrown if an IO error occurs */ public boolean hasNext() throws IOException; - + /** * Return true if a Record is available in the reverse direction * @throws IOException thrown if an IO error occurs */ public boolean hasPrevious() throws IOException; - + /** - * Return the nexy Record or null if one is not available. + * Return the next Record or null if one is not available. * @throws IOException thrown if an IO error occurs */ public DBRecord next() throws IOException; - + /** * Return the previous Record or null if one is not available. * @throws IOException thrown if an IO error occurs */ public DBRecord previous() throws IOException; - + /** * Delete the last Record read via the next or previous methods. * @return true if record was successfully deleted. diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramDB.java index 9503e1c095..bcf87805aa 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramDB.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -44,6 +44,7 @@ import ghidra.program.database.properties.DBPropertyMapManager; import ghidra.program.database.references.ReferenceDBManager; import ghidra.program.database.register.ProgramRegisterContextDB; import ghidra.program.database.reloc.RelocationManager; +import ghidra.program.database.sourcemap.SourceFileManagerDB; import ghidra.program.database.symbol.*; import ghidra.program.database.util.AddressSetPropertyMapDB; import ghidra.program.model.address.*; @@ -112,8 +113,9 @@ public class ProgramDB extends DomainObjectAdapterDB implements Program, ChangeM * unused flag bits. * 19-Oct-2023 - version 28 Revised overlay address space table and eliminated min/max. * Multiple blocks are permitted within a single overlay space. + * 13-Dec-2024 - version 29 Added source file manager. */ - static final int DB_VERSION = 28; + static final int DB_VERSION = 29; /** * UPGRADE_REQUIRED_BFORE_VERSION should be changed to DB_VERSION anytime the @@ -184,8 +186,9 @@ public class ProgramDB extends DomainObjectAdapterDB implements Program, ChangeM private static final int PROPERTY_MGR = 11; private static final int TREE_MGR = 12; private static final int RELOC_MGR = 13; + private static final int SOURCE_FILE_MGR = 14; - private static final int NUM_MANAGERS = 14; + private static final int NUM_MANAGERS = 15; private ManagerDB[] managers = new ManagerDB[NUM_MANAGERS]; private OldFunctionManager oldFunctionMgr; @@ -613,6 +616,11 @@ public class ProgramDB extends DomainObjectAdapterDB implements Program, ChangeM return (RelocationManager) managers[RELOC_MGR]; } + @Override + public SourceFileManagerDB getSourceFileManager() { + return (SourceFileManagerDB) managers[SOURCE_FILE_MGR]; + } + @Override public String getCompiler() { String compiler = null; @@ -1793,6 +1801,14 @@ public class ProgramDB extends DomainObjectAdapterDB implements Program, ChangeM versionExc = e.combine(versionExc); } + try { + managers[SOURCE_FILE_MGR] = + new SourceFileManagerDB(dbh, addrMap, openMode, lock, monitor); + } + catch (VersionException e) { + versionExc = e.combine(versionExc); + } + monitor.checkCancelled(); return versionExc; diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFile.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFile.java new file mode 100644 index 0000000000..2a435d800b --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFile.java @@ -0,0 +1,261 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database.sourcemap; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Instant; +import java.util.Arrays; +import java.util.HexFormat; + +import org.apache.commons.lang3.StringUtils; + +import ghidra.util.BigEndianDataConverter; + +/** + * A SourceFile is an immutable object representing a source file. It contains an + * absolute path along with an optional {@link SourceFileIdType} and identifier. + * For example, if the id type is {@link SourceFileIdType#MD5}, the identifier would + * be the md5 sum of the source file (stored as a byte array). + *

    + * Note: path parameters are assumed to be absolute file paths with forward slashes as the + * separator. For other cases, e.g. windows paths, consider the static convenience methods in + * the {@code SourceFileUtils} class. + *

    + * Note: you can use {@code SourceFileUtils.hexStringToByteArray} to convert hex Strings to byte + * arrays. You can use {@code SourceFileUtils.longToByteArray} to convert long values to the + * appropriate byte arrays. + */ +public final class SourceFile implements Comparable { + + private static final String FILE_SCHEME = "file"; + private static HexFormat hexFormat = HexFormat.of(); + private final String path; + private final String filename; + private final SourceFileIdType idType; + private final byte[] identifier; + private final int hash; + private final String idDisplayString; + + + /** + * Constructor requiring only a path. The path will be normalized (see {@link URI#normalize}) + * The id type will be set to {@code SourceFileIdType.NONE} and the identifier will + * be set to an array of length 0. + * + * @param path path + */ + public SourceFile(String path) { + this(path, SourceFileIdType.NONE, null, true); + } + + /** + * Constructor. The path will be normalized (see {@link URI#normalize}). + *

    + * Note: if {@code type} is {@code SourceFileIdType.NONE}, the {@code identifier} + * parameter is ignored. + *

    + * Note: use {@code SourceFileUtils.longToByteArray} to convert a {@code long} value + * to the appropriate {@code byte} array. + * @param path path + * @param type id type + * @param identifier id + */ + public SourceFile(String path, SourceFileIdType type, byte[] identifier) { + this(path, type, identifier, true); + } + + /** + * Constructor. The path will be normalized (see {@link URI#normalize}). + *

    + * Note: if {@code type} is {@code SourceFileIdType.NONE}, the {@code identifier} + * parameter is ignored. + *

    + * IMPORTANT: only pass {@code false} as {@code validate} parameter if you are certain that + * validation can be skipped, e.g., you are creating a SourceFile from information + * read out of the database which was validated at insertion. + * @param pathToValidate path + * @param type sourcefile id type + * @param identifier identifier + * @param validate true if params should be validated + */ + SourceFile(String pathToValidate, SourceFileIdType type, byte[] identifier, boolean validate) { + if (validate) { + if (StringUtils.isBlank(pathToValidate)) { + throw new IllegalArgumentException("pathToValidate cannot be null or blank"); + } + try { + URI uri = new URI(FILE_SCHEME, null, pathToValidate, null).normalize(); + path = uri.getPath(); + if (path.endsWith("/")) { + throw new IllegalArgumentException( + "SourceFile URI must represent a file (not a directory)"); + } + } + catch (URISyntaxException e) { + throw new IllegalArgumentException("path not valid: " + e.getMessage()); + } + } + else { + path = pathToValidate; + } + this.idType = type; + filename = path.substring(path.lastIndexOf("/") + 1); + this.identifier = validateAndCopyIdentifier(identifier); + hash = computeHashcode(); + idDisplayString = computeIdDisplayString(); + } + + /** + * Returns a file URI for this SourceFile. + * @return uri + */ + public URI getUri() { + try { + return new URI(FILE_SCHEME, null, path, null); + } + catch (URISyntaxException e) { + throw new AssertionError("URISyntaxException on validated path"); + } + } + + /** + * Returns the path + * @return path + */ + public String getPath() { + return path; + } + + /** + * Returns the filename + * @return filename + */ + public String getFilename() { + return filename; + } + + /** + * Returns the source file identifier type + * @return id type + */ + public SourceFileIdType getIdType() { + return idType; + } + + /** + * Returns (a copy of) the identifier + * @return identifier + */ + public byte[] getIdentifier() { + byte[] copy = new byte[identifier.length]; + System.arraycopy(identifier, 0, copy, 0, identifier.length); + return copy; + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SourceFile otherFile)) { + return false; + } + if (!path.equals(otherFile.path)) { + return false; + } + if (!idType.equals(otherFile.idType)) { + return false; + } + return Arrays.equals(identifier, otherFile.identifier); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(path); + if (idType.equals(SourceFileIdType.NONE)) { + return sb.toString(); + } + sb.append(" ["); + sb.append(idType.name()); + sb.append("="); + sb.append(getIdAsString()); + sb.append("]"); + return sb.toString(); + } + + @Override + public int compareTo(SourceFile sourceFile) { + int comp = path.compareTo(sourceFile.path); + if (comp != 0) { + return comp; + } + comp = idType.compareTo(sourceFile.idType); + if (comp != 0) { + return comp; + } + return Arrays.compare(identifier, sourceFile.identifier); + } + + /** + * Returns a String representation of the identifier + * @return id display string + */ + public String getIdAsString() { + return idDisplayString; + } + + // immutable object - compute hash and cache + private int computeHashcode() { + int result = path.hashCode(); + result = 31 * result + idType.hashCode(); + result = 31 * result + Arrays.hashCode(identifier); + return result; + } + + private byte[] validateAndCopyIdentifier(byte[] array) { + if (array == null || idType == SourceFileIdType.NONE) { + array = new byte[0]; + } + if (array.length > SourceFileIdType.MAX_LENGTH) { + throw new IllegalArgumentException( + "identifier array too long; max is " + SourceFileIdType.MAX_LENGTH); + } + if (idType.getByteLength() != 0 && idType.getByteLength() != array.length) { + throw new IllegalArgumentException( + "identifier array has wrong length for " + idType.name()); + } + byte[] copy = new byte[array.length]; + System.arraycopy(array, 0, copy, 0, array.length); + return copy; + } + + private String computeIdDisplayString() { + switch (idType) { + case NONE: + return StringUtils.EMPTY; + case TIMESTAMP_64: + return Instant.ofEpochMilli(BigEndianDataConverter.INSTANCE.getLong(identifier)) + .toString(); + default: + return hexFormat.formatHex(identifier); + } + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFileAdapter.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFileAdapter.java new file mode 100644 index 0000000000..ae1642a350 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFileAdapter.java @@ -0,0 +1,90 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database.sourcemap; + +import java.io.IOException; + +import db.*; +import ghidra.framework.data.OpenMode; +import ghidra.util.exception.VersionException; +import ghidra.util.task.TaskMonitor; + +/** + * Base class for adapters to access the Source File table. The table has one column, which stores + * the path of the source file (as a String). + */ +abstract class SourceFileAdapter { + + static final String TABLE_NAME = "SourceFiles"; + static final int PATH_COL = SourceFileAdapterV0.V0_PATH_COL; + static final int ID_TYPE_COL = SourceFileAdapterV0.V0_ID_TYPE_COL; + static final int ID_COL = SourceFileAdapterV0.V0_ID_COL; + + /** + * Creates an adapter for the source file table. + * @param handle database handle + * @param openMode open mode + * @param monitor task monitor + * @return adapter for table + * @throws VersionException if version incompatible + */ + static SourceFileAdapter getAdapter(DBHandle handle, OpenMode openMode, TaskMonitor monitor) + throws VersionException { + return new SourceFileAdapterV0(handle, openMode); + } + + /** + * Returns a {@link RecordIterator} for this table. + * @return record iterator + * @throws IOException on db error + */ + abstract RecordIterator getRecords() throws IOException; + + /** + * Returns the {@link DBRecord} corresponding to {@code sourceFile}, or {@code null} if + * no such record exists. + * @param sourceFile source file + * @return record or null + * @throws IOException on db error + */ + abstract DBRecord getRecord(SourceFile sourceFile) throws IOException; + + /** + * Returns the {@link DBRecord} with key {@code id}, or {@code null} if no such record exists. + * @param id id + * @return record or null + * @throws IOException on db error + */ + abstract DBRecord getRecord(long id) throws IOException; + + /** + * Creates a {@link DBRecord} for {@link SourceFile} {@code sourceFile}. If a record for + * that source file already exists, the existing record is returned. + * @param sourceFile source file + * @return db record + * @throws IOException on db error + */ + abstract DBRecord createSourceFileRecord(SourceFile sourceFile) throws IOException; + + /** + * Deletes the record with id {@code id} from the database. + * @param id id to delete + * @return true if deleted successfully + * @throws IOException on database error + */ + abstract boolean removeSourceFileRecord(long id) throws IOException; + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFileAdapterV0.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFileAdapterV0.java new file mode 100644 index 0000000000..b8be950e7b --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFileAdapterV0.java @@ -0,0 +1,149 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database.sourcemap; + +import java.io.IOException; +import java.util.Arrays; + +import db.*; +import ghidra.framework.data.OpenMode; +import ghidra.program.database.util.EmptyRecordIterator; +import ghidra.util.exception.VersionException; + +/** + * Initial version of {@link SourceFileAdapter}. + */ +class SourceFileAdapterV0 extends SourceFileAdapter implements DBListener { + + final static int SCHEMA_VERSION = 0; + static final int V0_PATH_COL = 0; + static final int V0_ID_TYPE_COL = 1; + static final int V0_ID_COL = 2; + + private final static Schema V0_SCHEMA = new Schema(SCHEMA_VERSION, "ID", + new Field[] { StringField.INSTANCE, ByteField.INSTANCE, BinaryField.INSTANCE }, + new String[] { "Path", "IdType", "Identifier" }, new int[] { V0_PATH_COL }); + + private Table table; // lazy creation, null if empty + private final DBHandle dbHandle; + private static final int[] INDEXED_COLUMNS = new int[] { V0_PATH_COL }; + + SourceFileAdapterV0(DBHandle dbHandle, OpenMode openMode) throws VersionException { + this.dbHandle = dbHandle; + + // As in FunctionTagAdapterV0, we need to add this as a database listener. + // Since the table is created lazily, undoing a transaction which (for example) caused + // the table to be created can leave the table in a bad state. + // The implementation of dbRestored(DBHandle) solves this issue. + this.dbHandle.addListener(this); + + if (!openMode.equals(OpenMode.CREATE)) { + table = dbHandle.getTable(TABLE_NAME); + if (table == null) { + return; // perform lazy table creation + } + int version = table.getSchema().getVersion(); + if (version != SCHEMA_VERSION) { + throw new VersionException(VersionException.NEWER_VERSION, false); + } + } + } + + @Override + public void dbRestored(DBHandle dbh) { + table = dbh.getTable(TABLE_NAME); + } + + @Override + public void dbClosed(DBHandle dbh) { + // nothing to do + } + + @Override + public void tableDeleted(DBHandle dbh, Table deletedTable) { + // nothing to do + } + + @Override + public void tableAdded(DBHandle dbh, Table addedTable) { + // nothing to do + } + + @Override + RecordIterator getRecords() throws IOException { + if (table == null) { + return new EmptyRecordIterator(); + } + return table.iterator(); + } + + @Override + DBRecord getRecord(long id) throws IOException { + if (table == null) { + return null; + } + return table.getRecord(id); + } + + @Override + DBRecord getRecord(SourceFile sourceFile) throws IOException { + if (table == null) { + return null; + } + StringField field = new StringField(sourceFile.getPath()); + RecordIterator iter = table.indexIterator(V0_PATH_COL, field, field, true); + while (iter.hasNext()) { + DBRecord rec = iter.next(); + if (rec.getByteValue(V0_ID_TYPE_COL) != sourceFile.getIdType().getIndex()) { + continue; + } + if (Arrays.equals(sourceFile.getIdentifier(), rec.getBinaryData(V0_ID_COL))) { + return rec; + } + } + return null; + + } + + @Override + DBRecord createSourceFileRecord(SourceFile sourceFile) throws IOException { + DBRecord rec = getRecord(sourceFile); + if (rec == null) { + rec = V0_SCHEMA.createRecord(getTable().getKey()); + rec.setString(V0_PATH_COL, sourceFile.getPath()); + rec.setByteValue(V0_ID_TYPE_COL, sourceFile.getIdType().getIndex()); + rec.setBinaryData(V0_ID_COL, sourceFile.getIdentifier()); + getTable().putRecord(rec); + } + return rec; + } + + @Override + boolean removeSourceFileRecord(long id) throws IOException { + if (table != null) { + return table.deleteRecord(id); + } + return false; + } + + private Table getTable() throws IOException { + if (table == null) { + table = dbHandle.createTable(TABLE_NAME, V0_SCHEMA, INDEXED_COLUMNS); + } + return table; + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFileIdType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFileIdType.java new file mode 100644 index 0000000000..b4b2dfa22e --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFileIdType.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.program.database.sourcemap; + +import java.util.HashMap; +import java.util.Map; + +/** + * An enum whose values represent source file id types, such as md5 or sha1. + */ +public enum SourceFileIdType { + NONE((byte) 0, 0), + UNKNOWN((byte) 1, 0), + TIMESTAMP_64((byte) 2, 8), + MD5((byte) 3, 16), + SHA1((byte) 4, 20), + SHA256((byte) 5, 32), + SHA512((byte) 6, 64); + + public static final int MAX_LENGTH = 64; + private static Map valueToEnum; + + static { + valueToEnum = new HashMap<>(); + for (SourceFileIdType type : SourceFileIdType.values()) { + valueToEnum.put(type.getIndex(), type); + } + } + + private final int byteLength; + private final byte index; + + private SourceFileIdType(byte index, int length) { + byteLength = length; + this.index = index; + } + + /** + * Returns the byte length of the corresponding identifier. A value of 0 indicates + * no restriction. + * + * @return byte length of identifier + */ + public int getByteLength() { + return byteLength; + } + + /** + * Returns the index of the identifier type. + * @return index + */ + byte getIndex() { + return index; + } + + /** + * Returns the id type given the index. + * @param index index + * @return id type + */ + static SourceFileIdType getTypeFromIndex(byte index) { + return valueToEnum.get(index); + + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFileManagerDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFileManagerDB.java new file mode 100644 index 0000000000..0c39f4f012 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceFileManagerDB.java @@ -0,0 +1,857 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database.sourcemap; + +import java.io.IOException; +import java.util.*; + +import db.*; +import db.util.ErrorHandler; +import ghidra.framework.data.OpenMode; +import ghidra.framework.store.LockException; +import ghidra.program.database.ManagerDB; +import ghidra.program.database.ProgramDB; +import ghidra.program.database.map.AddressMapDB; +import ghidra.program.model.address.*; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.model.sourcemap.*; +import ghidra.program.util.ProgramEvent; +import ghidra.util.Lock; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.VersionException; +import ghidra.util.task.TaskMonitor; + +/** + * Database Manager for managing source files and source map information. + */ +public class SourceFileManagerDB implements SourceFileManager, ManagerDB, ErrorHandler { + + private ProgramDB program; + + private SourceFileAdapter sourceFileTableAdapter; + private SourceMapAdapter sourceMapTableAdapter; + private AddressMapDB addrMap; + + protected final Lock lock; + + private Long lastKey; + private SourceFile lastSourceFile; + + /** + * Constructor + * @param dbh database handle + * @param addrMap map longs to addresses + * @param openMode mode + * @param lock program synchronization lock + * @param monitor task monitor + * @throws VersionException if the database is incompatible with the current schema + */ + public SourceFileManagerDB(DBHandle dbh, AddressMapDB addrMap, OpenMode openMode, Lock lock, + TaskMonitor monitor) throws VersionException { + this.addrMap = addrMap; + sourceFileTableAdapter = SourceFileAdapter.getAdapter(dbh, openMode, monitor); + sourceMapTableAdapter = SourceMapAdapter.getAdapter(dbh, addrMap, openMode, monitor); + this.lock = lock; + } + + @Override + public void setProgram(ProgramDB program) { + this.program = program; + } + + @Override + public void programReady(OpenMode openMode, int currentRevision, TaskMonitor monitor) + throws IOException, CancelledException { + // nothing to do + } + + @Override + public void invalidateCache(boolean all) throws IOException { + lastKey = null; + lastSourceFile = null; + } + + /** + * {@inheritDoc}
    + * Note: this method will split any source map entries that intersect the address + * range but are not entirely contained within it. Parts within the range to delete will + * be deleted. + * @param start first address in range + * @param end last address in range + * @param monitor task monitor + * @throws CancelledException if {@code monitor} is cancelled + */ + @Override + public void deleteAddressRange(Address start, Address end, TaskMonitor monitor) + throws CancelledException { + + AddressRange.checkValidRange(start, end); + AddressRange rangeToDelete = new AddressRangeImpl(start, end); + lock.acquire(); + try { + + RecordIterator recIter = sourceMapTableAdapter.getSourceMapRecordIterator(end, false); + boolean sourceMapChanged = false; + + // Note: we iterate backwards since records are stored based on the start address. + // We need to delete records stored before the beginning of the delete range that + // overlap the delete range, but we will never need to delete records that start after + // the delete range. + List entriesToCreate = new ArrayList<>(); + while (recIter.hasPrevious()) { + monitor.checkCancelled(); + DBRecord rec = recIter.previous(); + Address recStart = getStartAddress(rec); + long recLength = getLength(rec); + + if (recLength == 0) { + // if length 0 entry is in range to delete, delete entry + // otherwise ignore + if (rangeToDelete.contains(recStart)) { + recIter.delete(); + } + continue; + } + + Address recEnd = getEndAddress(recStart,recLength); + + if (!rangeToDelete.intersects(recStart, recEnd)) { + // we've found an entry that does not touch the range to delete + // this means we've handled all relevant entries and can stop + break; + } + + long fileAndLine = getFileAndLine(rec); + long fileId = fileAndLine >> 32; + int lineNum = (int) (fileAndLine & 0xffffffff); + if (isLessInSameSpace(recStart, start)) { + // rangeToDelete intersects (recStart,recEnd), so + // the entry must overlap the left endpoint of rangeToDelete + long length = start.subtract(recStart); + SourceMapEntryData data = + new SourceMapEntryData(fileId, lineNum, recStart, length); + entriesToCreate.add(data); + } + + if (isLessInSameSpace(end, recEnd)) { + // rangeToDelete intersects (recStart,recEnd) + // entry must overlap right endpoint of rangeToDelete + long length = recEnd.subtract(end); + SourceMapEntryData data = + new SourceMapEntryData(fileId, lineNum, end.add(1), length); + entriesToCreate.add(data); + } + recIter.delete(); + } + // add the new entries + for (SourceMapEntryData data : entriesToCreate) { + sourceMapTableAdapter.addMapEntry(data.sourceFileId, data.lineNumber, + data.baseAddress, data.length); + } + if (sourceMapChanged) { + program.setChanged(ProgramEvent.SOURCE_MAP_CHANGED, null, null); + } + } + catch (IOException e) { + dbError(e); + } + finally { + lock.release(); + } + } + + /** + * {@inheritDoc}
    + * Note: this method will move any source map entry which is entirely contained within the + * source address range. Entries which also contain addresses outside of this range will + * be split and parts within the range to move will be moved. + * @param fromAddr first address of range to be moved + * @param toAddr target address + * @param length number of addresses to move + * @param monitor task monitor + * @throws AddressOverflowException if overflow occurs when computing new addresses + * @throws CancelledException if {@code monitor} is cancelled + */ + @Override + public void moveAddressRange(Address fromAddr, Address toAddr, long length, TaskMonitor monitor) + throws AddressOverflowException, CancelledException { + + if (length < 0) { + throw new IllegalArgumentException( + "Invalid negative length for moveAddressRange: " + length); + } + + if (length == 0) { + return; // nothing to do + } + + lock.acquire(); + + try { + Address rangeToMoveEnd = fromAddr.addNoWrap(length - 1); + AddressRange rangeToMove = new AddressRangeImpl(fromAddr, rangeToMoveEnd); + RecordIterator recIter = + sourceMapTableAdapter.getSourceMapRecordIterator(rangeToMoveEnd, false); + boolean mapChanged = false; + + // Note: we iterate backwards since records are stored based on the start address. + // We need to move records stored before the beginning of rangeToMove that + // overlap rangeToMove but we will never need to move records that start after + // rangeToMove. + List entriesToCreate = new ArrayList<>(); + while (recIter.hasPrevious()) { + monitor.checkCancelled(); + DBRecord rec = recIter.previous(); + long entryLength = getLength(rec); + if (entryLength == 0) { + continue; // nothing to check + } + Address recStart = getStartAddress(rec); + Address recEnd = getEndAddress(recStart, entryLength); + + if (!rangeToMove.intersects(recStart, recEnd)) { + // we've found an entry entirely before rangeToMove + // this means we can stop looking + break; + } + long fileAndLine = getFileAndLine(rec); + long fileId = fileAndLine >> 32; + int lineNum = (int) (fileAndLine & 0xffffffff); + + if (isLessInSameSpace(recStart, fromAddr) && + isLessInSameSpace(rangeToMoveEnd, recEnd)) { + // entry extends over left and right endpoint of rangeToMove + long newLength = fromAddr.subtract(recStart); + SourceMapEntryData left = + new SourceMapEntryData(fileId, lineNum, recStart, newLength); + entriesToCreate.add(left); + SourceMapEntryData middle = + new SourceMapEntryData(fileId, lineNum, fromAddr, length); + entriesToCreate.add(middle); + newLength = recEnd.subtract(rangeToMoveEnd); + SourceMapEntryData right = + new SourceMapEntryData(fileId, lineNum, rangeToMoveEnd.add(1), newLength); + entriesToCreate.add(right); + recIter.delete(); + continue; + } + + if (isLessInSameSpace(recStart, fromAddr)) { + // entry extends before left endpoint (but not past right) + long newLength = fromAddr.subtract(recStart); + SourceMapEntryData left = + new SourceMapEntryData(fileId, lineNum, recStart, newLength); + entriesToCreate.add(left); + newLength = recEnd.subtract(fromAddr) + 1; + SourceMapEntryData middle = + new SourceMapEntryData(fileId, lineNum, fromAddr, newLength); + entriesToCreate.add(middle); + recIter.delete(); + continue; + } + + if (isLessInSameSpace(rangeToMoveEnd, recEnd)) { + // entry extends past right endpoint (but not before left) + long newLength = rangeToMoveEnd.subtract(recStart) + 1; + SourceMapEntryData middle = + new SourceMapEntryData(fileId, lineNum, recStart, newLength); + entriesToCreate.add(middle); + newLength = recEnd.subtract(rangeToMoveEnd); + SourceMapEntryData right = + new SourceMapEntryData(fileId, lineNum, rangeToMoveEnd.add(1), newLength); + entriesToCreate.add(right); + recIter.delete(); + continue; + } + // entry is entirely within range to move, no adjustment needed + mapChanged = true; + + } + + // add the new entries + for (SourceMapEntryData data : entriesToCreate) { + sourceMapTableAdapter.addMapEntry(data.sourceFileId, data.lineNumber, + data.baseAddress, data.length); + } + + mapChanged = mapChanged || !entriesToCreate.isEmpty(); + sourceMapTableAdapter.moveAddressRange(fromAddr, toAddr, length, monitor); + + if (mapChanged) { + program.setChanged(ProgramEvent.SOURCE_MAP_CHANGED, null, null); + } + } + catch (IOException e) { + dbError(e); + } + finally { + lock.release(); + } + } + + @Override + public boolean addSourceFile(SourceFile sourceFile) throws LockException { + Objects.requireNonNull(sourceFile, "sourceFile cannot be null"); + program.checkExclusiveAccess(); + lock.acquire(); + try { + Long key = getKeyForSourceFile(sourceFile); + if (key != null) { + return false; + } + DBRecord dbRecord = sourceFileTableAdapter.createSourceFileRecord(sourceFile); + updateLastSourceFileAndLastKey(dbRecord); + program.setObjChanged(ProgramEvent.SOURCE_FILE_ADDED, null, null, sourceFile); + return true; + } + catch (IOException e) { + dbError(e); + return false; + } + finally { + lock.release(); + } + } + + @Override + public boolean removeSourceFile(SourceFile sourceFile) throws LockException { + Objects.requireNonNull(sourceFile, "sourceFile cannot be null"); + boolean mapChanged = false; + program.checkExclusiveAccess(); + lock.acquire(); + try { + Long key = getKeyForSourceFile(sourceFile); + if (key == null) { + return false; + } + RecordIterator recIter = + sourceMapTableAdapter.getRecordsForSourceFile(key, 0, Integer.MAX_VALUE); + while (recIter.hasNext()) { + recIter.next(); + recIter.delete(); + mapChanged = true; + } + sourceFileTableAdapter.removeSourceFileRecord(key); + lastKey = null; + lastSourceFile = null; + } + catch (IOException e) { + dbError(e); + } + finally { + lock.release(); + } + program.setObjChanged(ProgramEvent.SOURCE_FILE_REMOVED, null, sourceFile, null); + if (mapChanged) { + program.setChanged(ProgramEvent.SOURCE_MAP_CHANGED, sourceFile, null); + } + return true; + } + + @Override + public List getSourceMapEntries(Address addr) { + + List sourceMapEntries = new ArrayList<>(); + + lock.acquire(); + try { + RecordIterator recIter = sourceMapTableAdapter.getSourceMapRecordIterator(addr, false); + boolean foundNonZeroLength = false; + while (recIter.hasPrevious()) { + DBRecord rec = recIter.previous(); + long entryLength = getLength(rec); + Address entryBase = getStartAddress(rec); + if (addr.equals(entryBase)) { + sourceMapEntries.add(getSourceMapEntry(rec)); + if (entryLength != 0) { + foundNonZeroLength = true; + } + continue; + } + if (entryLength == 0) { + continue; // only want length zero entries if they are based at addr + } + if (!foundNonZeroLength && + isLessOrEqualInSameSpace(addr, getEndAddress(entryBase, entryLength))) { + sourceMapEntries.add(getSourceMapEntry(rec)); + continue; // continue in case there are additional entries at entryBase + } + break; + } + } + catch (IOException e) { + dbError(e); + } + finally { + lock.release(); + } + Collections.sort(sourceMapEntries); + return sourceMapEntries; + } + + @Override + public SourceMapEntry addSourceMapEntry(SourceFile sourceFile, int lineNumber, Address baseAddr, + long length) throws LockException, AddressOverflowException { + if (lineNumber < 0) { + throw new IllegalArgumentException("lineNumber cannot be negative"); + } + if (length < 0) { + throw new IllegalArgumentException("length cannot be negative"); + } + Objects.requireNonNull(sourceFile, "sourceFile cannot be null"); + Objects.requireNonNull(baseAddr, "baseAddr cannot be null"); + MemoryBlock startBlock = program.getMemory().getBlock(baseAddr); + if (startBlock == null) { + throw new AddressOutOfBoundsException(baseAddr + " is not in a defined memory block"); + } + program.checkExclusiveAccess(); + + lock.acquire(); + try { + Long sourceFileId = getKeyForSourceFile(sourceFile); + if (sourceFileId == null) { + throw new IllegalArgumentException( + sourceFile.toString() + " not associated with program"); + } + if (length == 0) { + return addZeroLengthEntry(sourceFileId, lineNumber, baseAddr); + } + Address endAddr = baseAddr.addNoWrap(length - 1); + + // check that the entry's range is entirely contained within defined memory blocks + if (!startBlock.contains(endAddr)) { + if (!program.getMemory().contains(baseAddr, endAddr)) { + throw new AddressOutOfBoundsException( + baseAddr + "," + endAddr + " spans undefined memory"); + } + } + SourceMapEntryDB entry = null; + + RecordIterator recIter = + sourceMapTableAdapter.getSourceMapRecordIterator(endAddr, false); + while (recIter.hasPrevious()) { + DBRecord rec = recIter.previous(); + long entryLength = getLength(rec); + if (entryLength == 0) { + continue; // length 0 entries can't conflict + } + Address entryBase = getStartAddress(rec); + if (entryBase.equals(baseAddr)) { + if (entryLength != length) { + throw new IllegalArgumentException( + "new entry must have the same length as existing entry"); + } + if ((sourceFileId << 32 | lineNumber) == getFileAndLine(rec)) { + return getSourceMapEntry(rec); // entry is already in the DB + } + continue; // non-conflicting entry found, continue checking + } + if (isLessOrEqualInSameSpace(baseAddr, entryBase)) { + throw new IllegalArgumentException( + "new entry would overlap entry " + getSourceMapEntry(rec).toString()); + } + if (isLessOrEqualInSameSpace(entryBase, baseAddr)) { + if (getEndAddress(entryBase, entryLength).compareTo(baseAddr) >= 0) { + throw new IllegalArgumentException( + "new entry would overlap entry " + getSourceMapEntry(rec).toString()); + } + } + break; // safe to add new entry + } + DBRecord rec = + sourceMapTableAdapter.addMapEntry(sourceFileId, lineNumber, baseAddr, length); + entry = new SourceMapEntryDB(this, rec, addrMap); + program.setChanged(ProgramEvent.SOURCE_MAP_CHANGED, null, entry); + return entry; + } + catch (IOException e) { + dbError(e); + throw new AssertionError("addSourceMapEntry unsuccessful - possible database error"); + } + finally { + lock.release(); + } + } + + @Override + public boolean intersectsSourceMapEntry(AddressSetView addrs) { + if (addrs == null || addrs.isEmpty()) { + return false; + } + lock.acquire(); + try { + for (AddressRangeIterator rangeIter = addrs.getAddressRanges(); rangeIter.hasNext();) { + AddressRange r = rangeIter.next(); + RecordIterator recIter = + sourceMapTableAdapter.getSourceMapRecordIterator(r.getMaxAddress(), false); + while (recIter.hasPrevious()) { + DBRecord rec = recIter.previous(); + Address entryStart = getStartAddress(rec); + if (r.contains(entryStart)) { + return true; + } + long length = getLength(rec); + if (length == 0) { + continue; + } + if (r.intersects(entryStart, getEndAddress(entryStart, length))) { + return true; + } + break; + } + } + return false; + } + catch (IOException e) { + dbError(e); + return false; + } + finally { + lock.release(); + } + } + + @Override + public void dbError(IOException e) throws RuntimeException { + program.dbError(e); + } + + @Override + public List getMappedSourceFiles() { + List sourceFiles = new ArrayList<>(); + lock.acquire(); + try { + for (RecordIterator sourceFileRecordIter = + sourceFileTableAdapter.getRecords(); sourceFileRecordIter + .hasNext();) { + DBRecord sourceFileRecord = sourceFileRecordIter.next(); + long key = sourceFileRecord.getKey(); + RecordIterator sourceMapEntryRecordIter = + sourceMapTableAdapter.getRecordsForSourceFile(key, 0, Integer.MAX_VALUE); + if (sourceMapEntryRecordIter.hasNext()) { + updateLastSourceFileAndLastKey(sourceFileRecord); + sourceFiles.add(lastSourceFile); + } + } + } + catch (IOException e) { + dbError(e); + } + finally { + lock.release(); + } + return sourceFiles; + } + + @Override + public List getAllSourceFiles() { + List sourceFiles = new ArrayList<>(); + lock.acquire(); + try { + for (RecordIterator recordIter = sourceFileTableAdapter.getRecords(); recordIter + .hasNext();) { + updateLastSourceFileAndLastKey(recordIter.next()); + sourceFiles.add(lastSourceFile); + } + } + catch (IOException e) { + dbError(e); + } + finally { + lock.release(); + } + return sourceFiles; + } + + @Override + public void transferSourceMapEntries(SourceFile source, SourceFile target) + throws LockException { + program.checkExclusiveAccess(); + + lock.acquire(); + try { + Long srcKey = getKeyForSourceFile(source); + if (srcKey == null) { + throw new IllegalArgumentException( + source.toString() + " is not associated with program"); + } + Long targetKey = getKeyForSourceFile(target); + if (targetKey == null) { + throw new IllegalArgumentException( + target.toString() + " is not associated with program"); + } + if (source.equals(target)) { + return; // transfer redundant + } + for (SourceMapEntry entry : getSourceMapEntries(source, 0, Integer.MAX_VALUE)) { + addSourceMapEntry(target, entry.getLineNumber(), entry.getBaseAddress(), + entry.getLength()); + removeSourceMapEntry(entry); // remove fires a SOURCE_MAP_CHANGED event + } + } + catch (AddressOverflowException e) { + // can't happen - entry ranges were validated upon insert + throw new AssertionError("bad address range in source map entry table"); + } + finally { + lock.release(); + } + } + + @Override + public SourceMapEntryIterator getSourceMapEntryIterator(Address address, boolean forward) { + try { + return new SourceMapEntryIteratorDB(this, + sourceMapTableAdapter.getSourceMapRecordIterator(address, forward), forward); + } + catch (IOException e) { + dbError(e); + } + return SourceMapEntryIterator.EMPTY_ITERATOR; + } + + @Override + public boolean containsSourceFile(SourceFile sourceFile) { + if (sourceFile == null) { + return false; + } + lock.acquire(); + try { + return getKeyForSourceFile(sourceFile) != null; + } + finally { + lock.release(); + } + } + + @Override + public List getSourceMapEntries(SourceFile sourceFile, int minLine, + int maxLine) { + if (minLine < 0) { + throw new IllegalArgumentException("minLine cannot be negative; was " + minLine); + } + if (maxLine < 0) { + throw new IllegalArgumentException("maxLine cannot be negative; was " + maxLine); + } + if (maxLine < minLine) { + throw new IllegalArgumentException("maxLine cannot be less than minLine"); + } + List entries = new ArrayList<>(); + lock.acquire(); + try { + Long key = getKeyForSourceFile(sourceFile); + if (key == null) { + return entries; + } + try { + RecordIterator recIter = + sourceMapTableAdapter.getRecordsForSourceFile(key, minLine, maxLine); + while (recIter.hasNext()) { + DBRecord rec = recIter.next(); + entries.add(getSourceMapEntry(rec)); + } + } + catch (IOException e) { + dbError(e); + } + } + finally { + lock.release(); + } + Collections.sort(entries); + return entries; + } + + @Override + public boolean removeSourceMapEntry(SourceMapEntry entry) throws LockException { + Objects.requireNonNull(entry, "entry cannot be null"); + program.checkExclusiveAccess(); + lock.acquire(); + try { + RecordIterator recIter = + sourceMapTableAdapter.getSourceMapRecordIterator(entry.getBaseAddress(), true); + while (recIter.hasNext()) { + DBRecord rec = recIter.next(); + long length = getLength(rec); + if (length != entry.getLength()) { + continue; + } + long fileAndLine = getFileAndLine(rec); + if (((int) (fileAndLine & 0xffffffff)) != entry.getLineNumber()) { + continue; + } + if (!(entry.getSourceFile().equals(getSourceFileFromKey(fileAndLine >> 32)))) { + continue; + } + sourceMapTableAdapter.removeRecord(rec.getKey()); + program.setChanged(ProgramEvent.SOURCE_MAP_CHANGED, entry, null); + return true; + + } + } + catch (IOException e) { + dbError(e); + return false; + } + finally { + lock.release(); + } + return false; + } + + SourceFile getSourceFile(long key) { + lock.acquire(); + try { + return getSourceFileFromKey(key); + } + finally { + lock.release(); + } + } + + SourceMapEntry getSourceMapEntry(DBRecord rec) { + return new SourceMapEntryDB(this, rec, addrMap); + } + + static boolean isLessOrEqualInSameSpace(Address addr1, Address addr2) { + if (!addr1.hasSameAddressSpace(addr2)) { + return false; + } + return addr1.compareTo(addr2) <= 0; + } + + static boolean isLessInSameSpace(Address addr1, Address addr2) { + if (!addr1.hasSameAddressSpace(addr2)) { + return false; + } + return addr1.compareTo(addr2) < 0; + } + + // acquire lock before invoking + private SourceMapEntry addZeroLengthEntry(long sourceFileId, int lineNumber, Address baseAddr) { + SourceMapEntry entry = null; + try { + RecordIterator recIter = + sourceMapTableAdapter.getSourceMapRecordIterator(baseAddr, true); + while (recIter.hasNext()) { + DBRecord rec = recIter.next(); + Address recAddress = getStartAddress(rec); + if (!recAddress.equals(baseAddr)) { + break; + } + if (getLength(rec) != 0) { + continue; + } + long fileAndLine = getFileAndLine(rec); + if ((sourceFileId << 32 | lineNumber) != fileAndLine) { + continue; + } + return getSourceMapEntry(rec); + } + DBRecord rec = sourceMapTableAdapter.addMapEntry(sourceFileId, lineNumber, baseAddr, 0); + entry = new SourceMapEntryDB(this, rec, addrMap); + program.setChanged(ProgramEvent.SOURCE_MAP_CHANGED, null, entry); + return entry; + } + catch (IOException e) { + dbError(e); + throw new AssertionError("addZeroLengthEntry unsuccessful - possible database error"); + } + } + + private long getLength(DBRecord rec) { + return rec.getLongValue(SourceMapAdapter.LENGTH_COL); + } + + private Address getStartAddress(DBRecord rec) { + return addrMap.decodeAddress(rec.getLongValue(SourceMapAdapter.BASE_ADDR_COL)); + } + + private long getFileAndLine(DBRecord rec) { + return rec.getLongValue(SourceMapAdapter.FILE_LINE_COL); + } + + // assumes that start and length are from a valid SourceMapEntry + // that is, start.add(length - 1) doesn't wrap and is in the same + // space + private Address getEndAddress(Address start, long length) { + if (length == 0) { + return start; + } + try { + return start.addNoWrap(length - 1); + } + catch (AddressOverflowException e) { + // shouldn't happen, but return space max to prevent possibility of wrapping + return start.getAddressSpace().getMaxAddress(); + } + } + + // acquire lock before invoking + private SourceFile getSourceFileFromKey(long key) { + if (lastKey == null || lastKey.longValue() != key) { + DBRecord dbRecord = null; + try { + dbRecord = sourceFileTableAdapter.getRecord(key); + } + catch (IOException e) { + dbError(e); + return null; + } + if (dbRecord == null) { + return null; + } + updateLastSourceFileAndLastKey(dbRecord); + } + return lastSourceFile; + } + + // acquire lock before invoking + private Long getKeyForSourceFile(SourceFile sourceFile) { + if (lastSourceFile == null || !sourceFile.equals(lastSourceFile)) { + DBRecord dbRecord = null; + try { + dbRecord = sourceFileTableAdapter.getRecord(sourceFile); + } + catch (IOException e) { + dbError(e); + return null; + } + if (dbRecord == null) { + return null; + } + updateLastSourceFileAndLastKey(dbRecord); + } + return lastKey; + } + + private void updateLastSourceFileAndLastKey(DBRecord dbRecord) { + lastKey = dbRecord.getKey(); + String path = dbRecord.getString(SourceFileAdapter.PATH_COL); + SourceFileIdType idType = + SourceFileIdType.getTypeFromIndex(dbRecord.getByteValue(SourceFileAdapter.ID_TYPE_COL)); + byte[] identifier = dbRecord.getBinaryData(SourceFileAdapter.ID_COL); + lastSourceFile = new SourceFile(path, idType, identifier, false); + } + + /** + * A record for storing information about new source map entries which must be created + * during {@link #moveAddressRange} or {@link #deleteAddressRange} + */ + private record SourceMapEntryData(long sourceFileId, int lineNumber, Address baseAddress, + long length) {} + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceMapAdapter.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceMapAdapter.java new file mode 100644 index 0000000000..ad588b43b9 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceMapAdapter.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.program.database.sourcemap; + +import java.io.IOException; + +import db.*; +import ghidra.framework.data.OpenMode; +import ghidra.program.database.map.AddressMapDB; +import ghidra.program.model.address.Address; +import ghidra.program.model.sourcemap.SourceFileManager; +import ghidra.program.model.sourcemap.SourceMapEntry; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.VersionException; +import ghidra.util.task.TaskMonitor; + +/** + * Base class for adapters to access the Source Map table. + *

    + * Each entry in the table corresponds to a single {@link SourceMapEntry} and so records + * a {@link SourceFile}, a line number, a base address, and a length. + *

    + * There are a number of restrictions on a {@link SourceMapEntry}, which are listed in that + * interface's top-level documentation. It is the responsibility of the {@link SourceFileManager} + * to enforce these restrictions. + */ +abstract class SourceMapAdapter { + + static final String TABLE_NAME = "SourceMap"; + static final int FILE_LINE_COL = SourceMapAdapterV0.V0_FILE_LINE_COL; + static final int BASE_ADDR_COL = SourceMapAdapterV0.V0_BASE_ADDR_COL; + static final int LENGTH_COL = SourceMapAdapterV0.V0_LENGTH_COL; + + /** + * Creates an adapter for the Source Map table. + * @param dbh database handle + * @param addrMap address map + * @param openMode mode + * @param monitor task monitor + * @return adapter for table + * @throws VersionException if version incompatible + */ + static SourceMapAdapter getAdapter(DBHandle dbh, AddressMapDB addrMap, OpenMode openMode, + TaskMonitor monitor) throws VersionException { + return new SourceMapAdapterV0(dbh, addrMap, openMode); + } + + /** + * Removes a record from the table + * @param key key of record to remove. + * @return true if the record was deleted successfully + * @throws IOException if database error occurs + */ + abstract boolean removeRecord(long key) throws IOException; + + /** + * Returns a {@link RecordIterator} based at {@code addr}. + * @param addr starting address + * @param before if true, initial position is before addr, otherwise after + * @return iterator + * @throws IOException if database error occurs + */ + abstract RecordIterator getSourceMapRecordIterator(Address addr, boolean before) + throws IOException; + + /** + * Returns a {@link RecordIterator} over all records for the source file with + * id {@code id}, subject to the line bounds {@code minLine} and {@code maxLine} + * @param fileId id of source file + * @param minLine minimum line number + * @param maxLine maximum line number + * @return iterator + * @throws IOException if database error occurs + */ + abstract RecordIterator getRecordsForSourceFile(long fileId, int minLine, int maxLine) + throws IOException; + + /** + * Adds an entry to the source map table. This method assumes that no address + * in the associated range has already been associated with this source file and + * line number. + * @param fileId source file id + * @param lineNum line number + * @param baseAddr minimum address of range + * @param length number of addresses in range + * @return record + * @throws IOException if database error occurs + */ + abstract DBRecord addMapEntry(long fileId, int lineNum, Address baseAddr, long length) + throws IOException; + + /** + * Updates all appropriate entries in the table when an address range is moved. + * @param fromAddr from address + * @param toAddr to address + * @param length number of addresses in moved range + * @param monitor task monitor + * @throws CancelledException if task cancelled + * @throws IOException if database error occurs + */ + abstract void moveAddressRange(Address fromAddr, Address toAddr, long length, + TaskMonitor monitor) throws CancelledException, IOException; +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceMapAdapterV0.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceMapAdapterV0.java new file mode 100644 index 0000000000..9f2f79bee0 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceMapAdapterV0.java @@ -0,0 +1,160 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database.sourcemap; + +import java.io.IOException; + +import db.*; +import ghidra.framework.data.OpenMode; +import ghidra.program.database.map.AddressIndexPrimaryKeyIterator; +import ghidra.program.database.map.AddressMapDB; +import ghidra.program.database.util.DatabaseTableUtils; +import ghidra.program.database.util.EmptyRecordIterator; +import ghidra.program.model.address.Address; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.VersionException; +import ghidra.util.task.TaskMonitor; + +/** + * Initial version of {@link SourceMapAdapter} + */ +class SourceMapAdapterV0 extends SourceMapAdapter implements DBListener { + + final static int SCHEMA_VERSION = 0; + static final int V0_FILE_LINE_COL = 0; // indexed + static final int V0_BASE_ADDR_COL = 1; // indexed + static final int V0_LENGTH_COL = 2; + + // key | ((32-bit source file id << 32) | 32-bit line number) | base addr | length + private final static Schema V0_SCHEMA = new Schema(SCHEMA_VERSION, "ID", + new Field[] { LongField.INSTANCE, LongField.INSTANCE, LongField.INSTANCE }, + new String[] { "fileAndLine", "baseAddress", "length" }, null); + + private static final int[] INDEXED_COLUMNS = new int[] { V0_FILE_LINE_COL, V0_BASE_ADDR_COL }; + + private Table table; // lazy creation, null if empty + private final DBHandle dbHandle; + private AddressMapDB addrMap; + + /** + * Creates an adapter for version 0 of the source map adapter + * @param dbh database handle + * @param addrMap address map + * @param openMode open mode + * @throws VersionException if version incompatible + */ + SourceMapAdapterV0(DBHandle dbh, AddressMapDB addrMap, OpenMode openMode) + throws VersionException { + this.dbHandle = dbh; + this.addrMap = addrMap; + + // As in FunctionTagAdapterV0, we need to add this as a database listener. + // Since the table is created lazily, undoing a transaction which (for example) caused + // the table to be created can leave the table in a bad state. + // The implementation of dbRestored(DBHandle) solves this issue. + this.dbHandle.addListener(this); + + if (!openMode.equals(OpenMode.CREATE)) { + table = dbHandle.getTable(TABLE_NAME); + if (table == null) { + return; // perform lazy table creation + } + int version = table.getSchema().getVersion(); + if (version != SCHEMA_VERSION) { + throw new VersionException(VersionException.NEWER_VERSION, false); + } + } + } + + @Override + public void dbRestored(DBHandle dbh) { + table = dbh.getTable(TABLE_NAME); + } + + @Override + public void dbClosed(DBHandle dbh) { + // nothing to do + } + + @Override + public void tableDeleted(DBHandle dbh, Table t) { + // nothing to do + } + + @Override + public void tableAdded(DBHandle dbh, Table t) { + // nothing to do + } + + @Override + boolean removeRecord(long key) throws IOException { + if (table != null) { + return table.deleteRecord(key); + } + return false; + } + + @Override + RecordIterator getSourceMapRecordIterator(Address addr, boolean before) throws IOException { + if (table == null || addr == null) { + return EmptyRecordIterator.INSTANCE; + } + AddressIndexPrimaryKeyIterator keyIter = + new AddressIndexPrimaryKeyIterator(table, V0_BASE_ADDR_COL, addrMap, addr, before); + return new KeyToRecordIterator(table, keyIter); + } + + @Override + RecordIterator getRecordsForSourceFile(long fileId, int minLine, int maxLine) + throws IOException { + if (table == null) { + return EmptyRecordIterator.INSTANCE; + } + fileId = fileId << 32; + LongField minField = new LongField(fileId | minLine); + LongField maxField = new LongField(fileId | maxLine); + return table.indexIterator(V0_FILE_LINE_COL, minField, maxField, true); + } + + @Override + DBRecord addMapEntry(long fileId, int lineNum, Address baseAddr, long length) + throws IOException { + DBRecord rec = V0_SCHEMA.createRecord(getTable().getKey()); + rec.setLongValue(V0_FILE_LINE_COL, (fileId << 32) | lineNum); + rec.setLongValue(V0_BASE_ADDR_COL, addrMap.getKey(baseAddr, true)); + rec.setLongValue(V0_LENGTH_COL, length); + table.putRecord(rec); + return rec; + } + + @Override + void moveAddressRange(Address fromAddr, Address toAddr, long length, TaskMonitor monitor) + throws CancelledException, IOException { + if (table == null) { + return; + } + DatabaseTableUtils.updateIndexedAddressField(table, V0_BASE_ADDR_COL, addrMap, fromAddr, + toAddr, length, null, monitor); + } + + private Table getTable() throws IOException { + if (table == null) { + table = dbHandle.createTable(TABLE_NAME, V0_SCHEMA, INDEXED_COLUMNS); + } + return table; + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceMapEntryDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceMapEntryDB.java new file mode 100644 index 0000000000..f4dc4d8e0d --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceMapEntryDB.java @@ -0,0 +1,158 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database.sourcemap; + +import java.util.Objects; + +import db.DBRecord; +import ghidra.program.database.map.AddressMapDB; +import ghidra.program.model.address.*; +import ghidra.program.model.sourcemap.SourceMapEntry; + +/** + * Database implementation of {@link SourceMapEntry} interface. + *

    + * Note: clients should drop and reacquire all SourceMapEntryDB objects upon undo/redo, + * ProgramEvent.SOURCE_MAP_CHANGED, and ProgramEvent.SOURCE_FILE_REMOVED. + */ +public class SourceMapEntryDB implements SourceMapEntry { + + private int lineNumber; + private SourceFile sourceFile; + private Address baseAddress; + private long length; + private AddressRange range = null; + + /** + * Creates a new SourceMapEntryDB + * @param manager source file manager + * @param record backing record + * @param addrMap address map + */ + SourceMapEntryDB(SourceFileManagerDB manager, DBRecord record, AddressMapDB addrMap) { + manager.lock.acquire(); + try { + long fileAndLine = record.getLongValue(SourceMapAdapter.FILE_LINE_COL); + lineNumber = (int) (fileAndLine & 0xffffffff); + sourceFile = manager.getSourceFile(fileAndLine >> 32); + long encodedAddress = record.getLongValue(SourceMapAdapter.BASE_ADDR_COL); + baseAddress = addrMap.decodeAddress(encodedAddress); + length = record.getLongValue(SourceMapAdapter.LENGTH_COL); + if (length != 0) { + Address max; + try { + max = baseAddress.addNoWrap(length - 1); + } + catch (AddressOverflowException e) { + // shouldn't happen, but return space max to prevent possibility of wrapping + max = baseAddress.getAddressSpace().getMaxAddress(); + } + range = new AddressRangeImpl(baseAddress, max); + } + } + finally { + manager.lock.release(); + } + } + + @Override + public int getLineNumber() { + return lineNumber; + } + + @Override + public SourceFile getSourceFile() { + return sourceFile; + } + + @Override + public Address getBaseAddress() { + return baseAddress; + } + + @Override + public AddressRange getRange() { + return range; + } + + @Override + public long getLength() { + return length; + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(getSourceFile().toString()); + sb.append(":"); + sb.append(getLineNumber()); + sb.append(" @ "); + sb.append(getBaseAddress().toString()); + sb.append(" ("); + sb.append(Long.toString(getLength())); + sb.append(")"); + return sb.toString(); + } + + @Override + public int compareTo(SourceMapEntry o) { + int sourceFileCompare = getSourceFile().compareTo(o.getSourceFile()); + if (sourceFileCompare != 0) { + return sourceFileCompare; + } + int lineCompare = Integer.compare(getLineNumber(), o.getLineNumber()); + if (lineCompare != 0) { + return lineCompare; + } + int addrCompare = getBaseAddress().compareTo(o.getBaseAddress()); + if (addrCompare != 0) { + return addrCompare; + } + return Long.compareUnsigned(getLength(), o.getLength()); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SourceMapEntry otherEntry)) { + return false; + } + if (lineNumber != otherEntry.getLineNumber()) { + return false; + } + if (!sourceFile.equals(otherEntry.getSourceFile())) { + return false; + } + if (!baseAddress.equals(otherEntry.getBaseAddress())) { + return false; + } + if (length != otherEntry.getLength()) { + return false; + } + if (!Objects.equals(range, otherEntry.getRange())) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int hashCode = lineNumber; + hashCode = 31 * hashCode + sourceFile.hashCode(); + hashCode = 31 * hashCode + baseAddress.hashCode(); + hashCode = 31 * hashCode + Long.hashCode(length); + return hashCode; + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceMapEntryIteratorDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceMapEntryIteratorDB.java new file mode 100644 index 0000000000..9351cce628 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/SourceMapEntryIteratorDB.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.program.database.sourcemap; + +import java.io.IOException; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import db.DBRecord; +import db.RecordIterator; +import ghidra.program.model.sourcemap.SourceMapEntry; +import ghidra.program.model.sourcemap.SourceMapEntryIterator; + +/** + * Database implementation of {@link SourceMapEntryIterator} + */ +public class SourceMapEntryIteratorDB implements SourceMapEntryIterator { + + private boolean forward; + private RecordIterator recIter; + private SourceFileManagerDB sourceManager; + private SourceMapEntry nextEntry; + + /** + * Constructor + * @param sourceManager source manager + * @param recIter record iterator + * @param forward direction to iterate + */ + SourceMapEntryIteratorDB(SourceFileManagerDB sourceManager, RecordIterator recIter, + boolean forward) { + this.sourceManager = sourceManager; + this.recIter = recIter; + this.forward = forward; + this.nextEntry = null; + } + + @Override + public boolean hasNext() { + if (nextEntry != null) { + return true; + } + sourceManager.lock.acquire(); + try { + boolean recIterNext = forward ? recIter.hasNext() : recIter.hasPrevious(); + if (!recIterNext) { + return false; + } + DBRecord rec = forward ? recIter.next() : recIter.previous(); + nextEntry = sourceManager.getSourceMapEntry(rec); + return true; + } + catch (IOException e) { + sourceManager.dbError(e); + return false; + } + finally { + sourceManager.lock.release(); + } + } + + @Override + public SourceMapEntry next() { + if (hasNext()) { + SourceMapEntry entryToReturn = nextEntry; + nextEntry = null; + return entryToReturn; + } + throw new NoSuchElementException(); + } + + @Override + public Iterator iterator() { + return this; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/util/AddressRangeMapDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/util/AddressRangeMapDB.java index b90bfa018d..8b663e2892 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/util/AddressRangeMapDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/util/AddressRangeMapDB.java @@ -105,7 +105,7 @@ public class AddressRangeMapDB implements DBListener { * @param errHandler database error handler * @param valueField specifies the type for the values stored in this map * @param indexed if true, values will be indexed allowing use of the - * getValueRangeIterator method + * {@link AddressRangeMapDB#getAddressSet(Field)} method. */ public AddressRangeMapDB(DBHandle dbHandle, AddressMap addressMap, Lock lock, String name, ErrorHandler errHandler, Field valueField, boolean indexed) { @@ -343,7 +343,8 @@ public class AddressRangeMapDB implements DBListener { } /** - * Returns set of addresses where the given value has been set + * Returns set of addresses where the given value has been set. + * This method may only be invoked on indexed {@link AddressRangeMapDB}s! * @param value the value to search for * @return set of addresses where the given value has been set */ diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Program.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Program.java index 0ddd9dc536..baafcf0fe4 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Program.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Program.java @@ -28,6 +28,7 @@ import ghidra.program.model.lang.*; import ghidra.program.model.mem.Memory; import ghidra.program.model.pcode.Varnode; import ghidra.program.model.reloc.RelocationTable; +import ghidra.program.model.sourcemap.SourceFileManager; import ghidra.program.model.symbol.*; import ghidra.program.model.util.AddressSetPropertyMap; import ghidra.program.model.util.PropertyMapManager; @@ -149,6 +150,14 @@ public interface Program extends DataTypeManagerDomainObject, ProgramArchitectur */ public BookmarkManager getBookmarkManager(); + /** + * Returns the program's {@link SourceFileManager}. + * @return the source file manager + */ + default public SourceFileManager getSourceFileManager() { + return SourceFileManager.DUMMY; + } + /** * Gets the default pointer size in bytes as it may be stored within the program listing. * @return default pointer size. @@ -535,4 +544,5 @@ public interface Program extends DataTypeManagerDomainObject, ProgramArchitectur * @return unique program ID */ public long getUniqueProgramID(); + } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/DummySourceFileManager.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/DummySourceFileManager.java new file mode 100644 index 0000000000..328bdbc62b --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/DummySourceFileManager.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.program.model.sourcemap; + +import java.util.Collections; +import java.util.List; + +import ghidra.framework.store.LockException; +import ghidra.program.database.sourcemap.SourceFile; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; + +/** + * A "dummy" implementation of {@link SourceFileManager}. + */ +public class DummySourceFileManager implements SourceFileManager { + + public DummySourceFileManager() { + // nothing to do + } + + @Override + public List getSourceMapEntries(Address addr) { + return Collections.emptyList(); + } + + @Override + public SourceMapEntry addSourceMapEntry(SourceFile sourceFile, int lineNumber, Address baseAddr, + long length) throws LockException { + throw new UnsupportedOperationException("Cannot add source map entries with this manager"); + } + + @Override + public boolean intersectsSourceMapEntry(AddressSetView addrs) { + return false; + } + + @Override + public List getAllSourceFiles() { + return Collections.emptyList(); + } + + @Override + public List getMappedSourceFiles() { + return Collections.emptyList(); + } + + @Override + public void transferSourceMapEntries(SourceFile source, SourceFile target) { + throw new UnsupportedOperationException( + "Dummy source file manager cannot transfer map info"); + + } + + @Override + public SourceMapEntryIterator getSourceMapEntryIterator(Address address, boolean forward) { + return SourceMapEntryIterator.EMPTY_ITERATOR; + } + + @Override + public List getSourceMapEntries(SourceFile sourceFile, int minLine, + int maxLine) { + return Collections.emptyList(); + } + + @Override + public boolean addSourceFile(SourceFile sourceFile) throws LockException { + throw new UnsupportedOperationException("cannot add source files to this manager"); + } + + @Override + public boolean removeSourceFile(SourceFile sourceFile) throws LockException { + throw new UnsupportedOperationException("cannot remove source files from this manager"); + } + + @Override + public boolean containsSourceFile(SourceFile sourceFile) { + return false; + } + + @Override + public boolean removeSourceMapEntry(SourceMapEntry entry) throws LockException { + throw new UnsupportedOperationException( + "cannot remove source map entries from this manager"); + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourceFileManager.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourceFileManager.java new file mode 100644 index 0000000000..c38b23eb66 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourceFileManager.java @@ -0,0 +1,199 @@ +/* ### + * 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.sourcemap; + +import java.util.List; + +import ghidra.framework.store.LockException; +import ghidra.program.database.sourcemap.SourceFile; +import ghidra.program.model.address.*; + +/** + * This interface defines methods for managing {@link SourceFile}s and {@link SourceMapEntry}s. + */ +public interface SourceFileManager { + + public static final SourceFileManager DUMMY = new DummySourceFileManager(); + + /** + * Returns a sorted list of {@link SourceMapEntry}s associated with an address {@code addr}. + * @param addr address + * @return line number + */ + public List getSourceMapEntries(Address addr); + + /** + * Adds a {@link SourceMapEntry} with {@link SourceFile} {@code sourceFile}, + * line number {@code lineNumber}, and {@link AddressRange} {@code range} to the program + * database. + *

    + * Entries with non-zero lengths must either cover the same address range or be disjoint. + * @param sourceFile source file + * @param lineNumber line number + * @param range address range + * @return created SourceMapEntry + * @throws LockException if invoked without exclusive access + * @throws IllegalArgumentException if the range of the new entry intersects, but does + * not equal, the range of an existing entry or if sourceFile was not previously added + * to the program. + * @throws AddressOutOfBoundsException if the range of the new entry contains addresses + * that are not in a defined memory block + */ + public default SourceMapEntry addSourceMapEntry(SourceFile sourceFile, int lineNumber, + AddressRange range) throws LockException { + try { + return addSourceMapEntry(sourceFile, lineNumber, range.getMinAddress(), + range.getLength()); + } + // can't happen + catch (AddressOverflowException e) { + throw new AssertionError("Address overflow with valid AddressRange"); + } + } + + /** + * Creates a {@link SourceMapEntry} with {@link SourceFile} {@code sourceFile}, + * line number {@code lineNumber}, and non-negative length {@code length} and + * adds it to the program database. + *

    + * Entries with non-zero lengths must either cover the same address range or be disjoint. + * @param sourceFile source file + * @param lineNumber line number + * @param baseAddr minimum address of range + * @param length number of addresses in range + * @return created SourceMapEntry + * @throws AddressOverflowException if baseAddr + length-1 overflows + * @throws LockException if invoked without exclusive access + * @throws IllegalArgumentException if the range of the new entry intersects, but does + * not equal, the range of an existing entry or if sourceFile was not previously added to the + * program. + * @throws AddressOutOfBoundsException if the range of the new entry contains addresses + * that are not in a defined memory block + */ + public SourceMapEntry addSourceMapEntry(SourceFile sourceFile, int lineNumber, Address baseAddr, + long length) throws AddressOverflowException, LockException; + + /** + * Returns {@code true} precisely when at least one {@link Address} in {@code addrs} has + * source map information. + * @param addrs addresses to check + * @return true when at least one address has source map info + */ + public boolean intersectsSourceMapEntry(AddressSetView addrs); + + /** + * Adds a {@link SourceFile} to this manager. A SourceFile must be added before it can be + * associated with any source map information. + * + * @param sourceFile source file to add (can't be null) + * @return true if this manager did not already contain sourceFile + * @throws LockException if invoked without exclusive access + */ + public boolean addSourceFile(SourceFile sourceFile) throws LockException; + + /** + * Removes a {@link SourceFile} from this manager. Any associated {@link SourceMapEntry}s will + * also be removed. + * @param sourceFile source file to remove + * @return true if sourceFile was in the manager + * @throws LockException if invoked without exclusive access + */ + public boolean removeSourceFile(SourceFile sourceFile) throws LockException; + + /** + * Returns true precisely when this manager contains {@code sourceFile}. + * @param sourceFile source file + * @return true if source file already added + */ + public boolean containsSourceFile(SourceFile sourceFile); + + /** + * Returns a {@link List} containing all {@link SourceFile}s of the program. + * @return source file list + */ + public List getAllSourceFiles(); + + /** + * Returns a {@link List} containing {@link SourceFile}s which are + * mapped to at least one address in the program + * @return mapped source file list + */ + public List getMappedSourceFiles(); + + /** + * Changes the source map so that any {@link SourceMapEntry} associated with {@code source} + * is associated with {@code target} instead. Any entries associated with + * {@code target} before invocation will still be associated with + * {@code target} after invocation. {@code source} will not be associated + * with any entries after invocation (unless {@code source} and {@code target} + * are the same). Line number information is not changed. + * @param source source file to get info from + * @param target source file to move info to + * @throws LockException if invoked without exclusive access + * @throws IllegalArgumentException if source or target has not been added previously + */ + public void transferSourceMapEntries(SourceFile source, SourceFile target) throws LockException; + + /** + * Returns a {@link SourceMapEntryIterator} starting at {@code address}. + * + * @param address starting address + * @param forward direction of iterator (true = forward) + * @return iterator + */ + public SourceMapEntryIterator getSourceMapEntryIterator(Address address, boolean forward); + + /** + * Returns the sorted list of {@link SourceMapEntry}s for {@code sourceFile} with line number + * between {@code minLine} and {@code maxLine}, inclusive. + * @param sourceFile source file + * @param minLine minimum line number + * @param maxLine maximum line number + * @return source map entries + */ + public List getSourceMapEntries(SourceFile sourceFile, int minLine, + int maxLine); + + /** + * Returns the sorted list of {@link SourceMapEntry}s for {@code sourceFile} with line number + * equal to {@code lineNumber}. + * @param sourceFile source file + * @param lineNumber line number + * @return source map entries + */ + public default List getSourceMapEntries(SourceFile sourceFile, int lineNumber) { + return getSourceMapEntries(sourceFile, lineNumber, lineNumber); + } + + /** + * Returns a sorted of list all {@link SourceMapEntry}s in the program corresponding to + * {@code sourceFile}. + * @param sourceFile source file + * @return source map entries + */ + public default List getSourceMapEntries(SourceFile sourceFile) { + return getSourceMapEntries(sourceFile, 0, Integer.MAX_VALUE); + } + + /** + * Removes a {@link SourceMapEntry} from this manager. + * @param entry entry to remove + * @return true if entry was in the manager + * @throws LockException if invoked without exclusive access + */ + public boolean removeSourceMapEntry(SourceMapEntry entry) throws LockException; + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourceMapEntry.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourceMapEntry.java new file mode 100644 index 0000000000..7af966ae39 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourceMapEntry.java @@ -0,0 +1,80 @@ +/* ### + * 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.sourcemap; + +import ghidra.program.database.sourcemap.SourceFile; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; + +/** + * A SourceMapEntry consists of a {@link SourceFile}, a line number, a base address, + * and a length. If the length is positive, the base address and the length determine + * an {@link AddressRange}. In this case, the length of a {@code SourceMapEntry} is the + * length of the associated {@link AddressRange}, i.e., the number of {@link Address}es in + * the range (see {@link AddressRange#getLength()}). The intent is that the range + * contains all of the bytes corresponding to a given line of source. The length of a + * {@code SourceMapEntry} can be 0, in which case the associated range is null. Negative + * lengths are not allowed. + *

    + * The baseAddress of a range must occur within a memory block of the program, as must each + * address within the range of a {@code SourceMapEntry}. A range may span multiple + * (contiguous) memory blocks. + *

    + * If the ranges of two entries (with non-zero lengths) intersect, then the ranges must be + * identical. The associated {@link SourceFile}s and/or line numbers can be different. + *

    + * Entries with length zero do not conflict with other entries and may occur within the + * range of another entry. + *

    + * For a fixed source file, line number, base address, and length, there must be only one + * SourceMapEntry. + *

    + * SourceMapEntry objects are created using the {@link SourceFileManager} for a program, + * which must enforce the restrictions listed above. + */ +public interface SourceMapEntry extends Comparable { + + /** + * Returns the line number. + * @return line number + */ + public int getLineNumber(); + + /** + * Returns the source file + * @return source file + */ + public SourceFile getSourceFile(); + + /** + * Returns the base address of the entry + * @return base address + */ + public Address getBaseAddress(); + + /** + * Returns the length of the range (number of addresses) + * @return length + */ + public long getLength(); + + /** + * Returns the address range, or null for length 0 entries + * @return address range or null + */ + public AddressRange getRange(); + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourceMapEntryIterator.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourceMapEntryIterator.java new file mode 100644 index 0000000000..67c7c7f5f9 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourceMapEntryIterator.java @@ -0,0 +1,44 @@ +/* ### + * 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.sourcemap; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Interface for iterating over {@link SourceMapEntry}s. + */ +public interface SourceMapEntryIterator extends Iterator, Iterable { + + public static final SourceMapEntryIterator EMPTY_ITERATOR = new SourceMapEntryIterator() { + + @Override + public Iterator iterator() { + return this; + } + + @Override + public SourceMapEntry next() { + throw new NoSuchElementException("Empty iterator is empty!"); + } + + @Override + public boolean hasNext() { + return false; + } + }; + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/symbol/SymbolUtilities.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/symbol/SymbolUtilities.java index 1950192e8f..9e6fdf3e38 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/symbol/SymbolUtilities.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/symbol/SymbolUtilities.java @@ -17,7 +17,6 @@ package ghidra.program.model.symbol; import java.util.*; import java.util.function.Consumer; -import java.util.stream.Stream; import ghidra.program.model.address.*; import ghidra.program.model.data.*; @@ -137,7 +136,7 @@ public class SymbolUtilities { /** * Check for invalid characters - * (space, colon, asterisk, plus, bracket) + * (space or unprintable ascii below 0x20) * in labels. * * @param str the string to be checked for invalid characters. diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/ProgramEvent.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/ProgramEvent.java index 8cffb56090..e3870e0f8f 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/ProgramEvent.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/ProgramEvent.java @@ -147,7 +147,11 @@ public enum ProgramEvent implements EventType { CODE_UNIT_USER_DATA_CHANGED, // user data has changed for some code unit USER_DATA_CHANGED, // general user data has changed at some address - RELOCATION_ADDED; // a relocation entry was added + RELOCATION_ADDED, // a relocation entry was added + + SOURCE_FILE_ADDED, // a source file was added + SOURCE_FILE_REMOVED, // a source file was removed + SOURCE_MAP_CHANGED; // source map information was changed private final int id = DomainObjectEventIdGenerator.next(); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/SourceMapFieldLocation.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/SourceMapFieldLocation.java new file mode 100644 index 0000000000..7f8062e5aa --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/SourceMapFieldLocation.java @@ -0,0 +1,47 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.util; + +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.program.model.sourcemap.SourceMapEntry; + +/** + * A {@link ProgramLocation} for source map information. + */ +public class SourceMapFieldLocation extends ProgramLocation { + + private SourceMapEntry sourceMapEntry; + + public SourceMapFieldLocation(Program program, Address addr, int row, int charOffset, + SourceMapEntry sourceMapEntry) { + super(program, addr, row, 0, charOffset); + this.sourceMapEntry = sourceMapEntry; + } + + public SourceMapFieldLocation() { + // for deserialization + } + + /** + * Returns the {@link SourceMapEntry} associated with this location. + * @return source map entry + */ + public SourceMapEntry getSourceMapEntry() { + return sourceMapEntry; + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/AbstractSourceFileTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/AbstractSourceFileTest.java new file mode 100644 index 0000000000..838f1f889f --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/AbstractSourceFileTest.java @@ -0,0 +1,108 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database.sourcemap; + +import org.junit.Before; + +import generic.test.AbstractGenericTest; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.sourcemap.SourceFileManager; +import ghidra.test.ToyProgramBuilder; + +public class AbstractSourceFileTest extends AbstractGenericTest { + + protected Program program; + protected ToyProgramBuilder builder; + protected int baseOffset = 0x1001000; + protected Address baseAddress; + protected SourceFile source1; + protected SourceFile source2; + protected SourceFile source3; + protected Instruction ret2_1; + protected Instruction ret2_2; + protected Instruction ret2_3; + protected Instruction nop1_1; + protected Instruction nop1_2; + protected Instruction nop1_3; + protected Instruction ret2_4; + protected Instruction nop1_4; + protected String path1 = "/test1"; + protected String path2 = "/test2/test2"; + protected String path3 = "/test3/test3/test3"; + protected String path4 = "/test4/test4/test4/test4"; + protected SourceFileManager sourceManager; + + @Before + public void setUp() throws Exception { + builder = new ToyProgramBuilder("testprogram", true, false, this); + int txID = builder.getProgram().startTransaction("create source map test program"); + long currentOffset = baseOffset; + try { + builder.createMemory(".text", Integer.toHexString(baseOffset), 64).setExecute(true); + builder.addBytesReturn(currentOffset); + currentOffset += 2; + builder.addBytesNOP(currentOffset++, 1); + builder.addBytesReturn(currentOffset); + currentOffset += 2; + builder.addBytesNOP(currentOffset++, 1); + builder.addBytesReturn(currentOffset); + currentOffset += 2; + builder.addBytesNOP(currentOffset++, 1); + builder.addBytesReturn(currentOffset); + currentOffset += 2; + builder.addBytesNOP(currentOffset++, 1); + builder.disassemble(Integer.toHexString(baseOffset), + (int) (currentOffset - baseOffset)); + } + finally { + builder.getProgram().endTransaction(txID, true); + } + program = builder.getProgram(); + baseAddress = program.getAddressFactory().getDefaultAddressSpace().getAddress(baseOffset); + InstructionIterator instIter = program.getListing().getInstructions(baseAddress, true); + ret2_1 = instIter.next(); + nop1_1 = instIter.next(); + ret2_2 = instIter.next(); + nop1_2 = instIter.next(); + ret2_3 = instIter.next(); + nop1_3 = instIter.next(); + ret2_4 = instIter.next(); + nop1_4 = instIter.next(); + sourceManager = program.getSourceFileManager(); + + source1 = new SourceFile(path1); + source2 = new SourceFile(path2); + source3 = new SourceFile(path3); + //leave path4 as String without a corresponding source file + + txID = program.startTransaction("adding source files"); + try { + sourceManager.addSourceFile(source1); + sourceManager.addSourceFile(source2); + sourceManager.addSourceFile(source3); + } + finally { + program.endTransaction(txID, true); + } + builder.dispose(); + } + + protected AddressRange getBody(CodeUnit cu) { + return new AddressRangeImpl(cu.getMinAddress(), cu.getMaxAddress()); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/GetSourceFilesTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/GetSourceFilesTest.java new file mode 100644 index 0000000000..013cdba42a --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/GetSourceFilesTest.java @@ -0,0 +1,131 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database.sourcemap; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import ghidra.framework.store.LockException; +import ghidra.program.model.sourcemap.SourceMapEntry; + +public class GetSourceFilesTest extends AbstractSourceFileTest { + + @Test + public void testGetAllSourceFiles() throws LockException { + List sourceFiles = sourceManager.getAllSourceFiles(); + assertEquals(3, sourceFiles.size()); + assertTrue(sourceFiles.contains(source1)); + assertTrue(sourceFiles.contains(source2)); + assertTrue(sourceFiles.contains(source3)); + + int txId = program.startTransaction("deleting source file 2"); + try { + sourceManager.removeSourceFile(source2); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getAllSourceFiles(); + assertEquals(2, sourceFiles.size()); + assertTrue(sourceFiles.contains(source1)); + assertTrue(sourceFiles.contains(source3)); + + txId = program.startTransaction("deleting source files"); + try { + sourceManager.removeSourceFile(source1); + sourceManager.removeSourceFile(source3); + } + finally { + program.endTransaction(txId, true); + } + + assertTrue(sourceManager.getAllSourceFiles().isEmpty()); + } + + @Test + public void testGetMappedSourceFiles() throws LockException { + List sourceFiles = sourceManager.getMappedSourceFiles(); + assertTrue(sourceFiles.isEmpty()); + + int txId = program.startTransaction("adding source map info"); + try { + sourceManager.addSourceMapEntry(source1, 1, getBody(ret2_1)); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(1, sourceFiles.size()); + SourceFile file = sourceFiles.get(0); + assertEquals(source1.getPath(), file.getPath()); + + txId = program.startTransaction("adding source map info"); + try { + sourceManager.addSourceMapEntry(source2, 1, getBody(ret2_2)); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(2, sourceFiles.size()); + assertTrue(sourceFiles.contains(source1)); + assertTrue(sourceFiles.contains(source2)); + + txId = program.startTransaction("transferring source map entries"); + try { + sourceManager.transferSourceMapEntries(source2, source3); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(2, sourceFiles.size()); + assertTrue(sourceFiles.contains(source1)); + assertTrue(sourceFiles.contains(source3)); + + txId = program.startTransaction("deleting source1"); + try { + sourceManager.removeSourceFile(source1); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(1, sourceFiles.size()); + assertTrue(sourceFiles.contains(source3)); + + txId = program.startTransaction("clearing mapping info for source3"); + try { + List entries = sourceManager.getSourceMapEntries(source3); + for (SourceMapEntry entry : entries) { + assertTrue(sourceManager.removeSourceMapEntry(entry)); + } + } + finally { + program.endTransaction(txId, true); + } + sourceFiles = sourceManager.getMappedSourceFiles(); + assertTrue(sourceFiles.isEmpty()); + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/MappingSourceFilesTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/MappingSourceFilesTest.java new file mode 100644 index 0000000000..a314f3d74b --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/MappingSourceFilesTest.java @@ -0,0 +1,1150 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database.sourcemap; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.HexFormat; +import java.util.List; + +import org.junit.Test; +import org.python.google.common.primitives.Longs; + +import ghidra.framework.store.LockException; +import ghidra.program.model.address.*; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.model.sourcemap.SourceFileManager; +import ghidra.program.model.sourcemap.SourceMapEntry; +import ghidra.util.SourceFileUtils; +import ghidra.util.SourceFileUtils.SourceLineBounds; + +public class MappingSourceFilesTest extends AbstractSourceFileTest { + + @Test + public void testNoSourceInfo() { + assertTrue(sourceManager.getMappedSourceFiles().isEmpty()); + assertEquals(3, sourceManager.getAllSourceFiles().size()); + assertTrue(sourceManager.getSourceMapEntries(ret2_1.getAddress()).isEmpty()); + assertTrue(sourceManager.getSourceMapEntries(source1).isEmpty()); + } + + @Test + public void testMappingDummySourceManager() { + sourceManager = SourceFileManager.DUMMY; + assertTrue(sourceManager.getMappedSourceFiles().isEmpty()); + assertTrue(sourceManager.getAllSourceFiles().isEmpty()); + + assertTrue(sourceManager.getSourceMapEntries(ret2_1.getMinAddress()).isEmpty()); + assertTrue(sourceManager.getSourceMapEntries(source1, 1, 1).isEmpty()); + assertTrue(sourceManager.getSourceMapEntries(source1, 0, Integer.MAX_VALUE).isEmpty()); + } + + @Test(expected = IllegalArgumentException.class) + public void testNegativeLength() throws AddressOverflowException, LockException { + int txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), -1); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = NullPointerException.class) + public void testAddingEntryNullSourceFile() throws AddressOverflowException, LockException { + int txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(null, 1, ret2_1.getAddress(), 1); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = NullPointerException.class) + public void testRemovingNullSourceMapEntry() throws LockException { + int txId = program.startTransaction("removing source map entry"); + try { + sourceManager.removeSourceMapEntry(null); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testGetSourceInfoNullAddress() { + assertTrue(sourceManager.getSourceMapEntries((Address) null).isEmpty()); + } + + @Test + public void testGetSourceInfoNoAddress() { + assertTrue(sourceManager.getSourceMapEntries(Address.NO_ADDRESS).isEmpty()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSettingNegativeLineNumber() throws LockException { + + int txId = program.startTransaction("setting bad line number info"); + try { + sourceManager.addSourceMapEntry(source1, -1, getBody(ret2_1)); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNegativeMinline() { + sourceManager.getSourceMapEntries(source1, -1, 10); + } + + @Test(expected = IllegalArgumentException.class) + public void testNegativeMaxline() { + sourceManager.getSourceMapEntries(source1, 1, -1); + } + + @Test(expected = IllegalArgumentException.class) + public void testMaxLineLessThanMin() { + sourceManager.getSourceMapEntries(source1, 10, 5); + } + + @Test + public void testAddingSameEntryTwice() throws LockException { + + int txId = program.startTransaction("adding source map entry"); + SourceMapEntry entry = null; + try { + entry = sourceManager.addSourceMapEntry(source1, 1, getBody(ret2_1)); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("adding redundant source map info"); + SourceMapEntry entry2 = null; + try { + entry2 = sourceManager.addSourceMapEntry(source1, 1, getBody(ret2_1)); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(entry, entry2); + assertEquals(1, sourceManager.getSourceMapEntries(source1).size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testAddingSimilarEntriesDifferentLengths() + throws LockException, AddressOverflowException { + int txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), 1); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("adding incompatible source map info"); + + try { + sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), 2); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testAddingSameLengthZeroEntryTwice() + throws LockException, AddressOverflowException { + + int txId = program.startTransaction("adding source map entry"); + SourceMapEntry entry = null; + try { + entry = sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), 0); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("adding redundant source map info"); + SourceMapEntry entry2 = null; + try { + entry2 = sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), 0); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(entry, entry2); + assertEquals(1, sourceManager.getSourceMapEntries(source1).size()); + } + + @Test + public void simpleSettingGettingTest() throws LockException, IOException { + assertTrue(sourceManager.getMappedSourceFiles().isEmpty()); + assertTrue(sourceManager.getSourceMapEntries(ret2_1.getAddress()).isEmpty()); + + int txId = program.startTransaction("adding source map entry"); + SourceMapEntry source1Entry = null; + try { + source1Entry = sourceManager.addSourceMapEntry(source1, 1, getBody(ret2_1)); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(2, source1Entry.getLength()); + assertEquals(1, source1Entry.getLineNumber()); + assertEquals(getBody(ret2_1), source1Entry.getRange()); + assertEquals(source1, source1Entry.getSourceFile()); + + List sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(1, sourceFiles.size()); + List entries = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(1, entries.size()); + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress().add(1)); + assertEquals(1, entries.size()); + entries = sourceManager.getSourceMapEntries(nop1_1.getAddress()); + assertEquals(0, entries.size()); + + txId = program.startTransaction("adding source map entry"); + SourceMapEntry source2Entry = null; + try { + source2Entry = sourceManager.addSourceMapEntry(source2, 2, getBody(ret2_2)); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(2, sourceFiles.size()); + assertTrue(sourceFiles.contains(source1)); + assertTrue(sourceFiles.contains(source2)); + + List entries1 = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(1, entries1.size()); + assertEquals(source1Entry, entries1.get(0)); + + List entries2 = sourceManager.getSourceMapEntries(ret2_2.getAddress()); + assertEquals(1, entries2.size()); + assertEquals(source2Entry, entries2.get(0)); + + program.undo(); + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(1, sourceFiles.size()); + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(1, entries.size()); + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress().add(1)); + assertEquals(1, entries.size()); + entries = sourceManager.getSourceMapEntries(nop1_1.getAddress()); + assertEquals(0, entries.size()); + + } + + @Test + public void testMultipleEntriesOneAddress() throws LockException { + assertTrue(sourceManager.getSourceMapEntries(ret2_1.getAddress()).isEmpty()); + AddressRange range = getBody(ret2_1); + + SourceMapEntry entry_1_1 = null; + SourceMapEntry entry_1_2 = null; + SourceMapEntry entry_2_1 = null; + + int txId = program.startTransaction("adding first source map entry"); + try { + entry_1_1 = sourceManager.addSourceMapEntry(source1, 1, range); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(0, sourceManager.getSourceMapEntries(ret2_1.getAddress().subtract(1)).size()); + + List entries = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(1, entries.size()); + assertTrue(entries.contains(entry_1_1)); + + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress().add(1)); + assertEquals(1, entries.size()); + assertTrue(entries.contains(entry_1_1)); + + assertEquals(0, sourceManager.getSourceMapEntries(ret2_1.getAddress().add(2)).size()); + + txId = program.startTransaction("adding second source map entry"); + try { + entry_1_2 = sourceManager.addSourceMapEntry(source1, 2, range); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(0, sourceManager.getSourceMapEntries(ret2_1.getAddress().subtract(1)).size()); + + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(2, entries.size()); + assertTrue(entries.contains(entry_1_1)); + assertTrue(entries.contains(entry_1_2)); + + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress().add(1)); + assertEquals(2, entries.size()); + assertTrue(entries.contains(entry_1_1)); + assertTrue(entries.contains(entry_1_2)); + + assertEquals(0, sourceManager.getSourceMapEntries(ret2_1.getAddress().add(2)).size()); + + txId = program.startTransaction("adding third source map entry"); + try { + entry_2_1 = sourceManager.addSourceMapEntry(source2, 1, range); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(0, sourceManager.getSourceMapEntries(ret2_1.getAddress().subtract(1)).size()); + + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(3, entries.size()); + assertTrue(entries.contains(entry_1_1)); + assertTrue(entries.contains(entry_1_2)); + assertTrue(entries.contains(entry_2_1)); + + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress().add(1)); + assertEquals(3, entries.size()); + assertTrue(entries.contains(entry_1_1)); + assertTrue(entries.contains(entry_1_2)); + assertTrue(entries.contains(entry_2_1)); + + assertEquals(0, sourceManager.getSourceMapEntries(ret2_1.getAddress().add(2)).size()); + + } + + @Test + public void testAddingLengthZeroEntries() throws AddressOverflowException, LockException { + assertTrue(sourceManager.getSourceMapEntries(ret2_1.getAddress()).isEmpty()); + SourceMapEntry entry1 = null; + SourceMapEntry entry2 = null; + SourceMapEntry entry3 = null; + SourceMapEntry entry4 = null; + + int txId = program.startTransaction("adding first source map entry"); + try { + entry1 = sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), 2); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("adding length zero entry"); + try { + entry2 = sourceManager.addSourceMapEntry(source1, 2, ret2_1.getAddress(), 0); + } + finally { + program.endTransaction(txId, true); + } + + assertNull(entry2.getRange()); + + txId = program.startTransaction("adding length zero entry"); + try { + entry3 = sourceManager.addSourceMapEntry(source1, 3, ret2_1.getAddress(), 0); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("adding length zero entry"); + try { + entry4 = sourceManager.addSourceMapEntry(source1, 4, ret2_1.getAddress().add(1L), 0); + } + finally { + program.endTransaction(txId, true); + } + + List containingEntries = + sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(3, containingEntries.size()); + assertTrue(containingEntries.contains(entry1)); + assertTrue(containingEntries.contains(entry2)); + assertTrue(containingEntries.contains(entry3)); + + containingEntries = sourceManager.getSourceMapEntries(ret2_1.getAddress().add(1L)); + assertEquals(2, containingEntries.size()); + assertTrue(containingEntries.contains(entry1)); + assertTrue(containingEntries.contains(entry4)); + } + + @Test + public void testRemovingEntries() throws LockException, IOException { + + Address min = ret2_1.getMinAddress(); + Address max = ret2_1.getMaxAddress(); + AddressRange range = new AddressRangeImpl(min, max); + SourceMapEntry entry = null; + + int txId = program.startTransaction("adding source map entry"); + try { + entry = sourceManager.addSourceMapEntry(source1, 1, range); + } + finally { + program.endTransaction(txId, true); + } + assertNotNull(entry); + assertEquals(1, sourceManager.getSourceMapEntries(min).size()); + assertEquals(1, sourceManager.getSourceMapEntries(max).size()); + + txId = program.startTransaction("removing source map entry"); + try { + assertTrue(sourceManager.removeSourceMapEntry(entry)); + } + finally { + program.endTransaction(txId, true); + } + + assertTrue(sourceManager.getSourceMapEntries(min).isEmpty()); + assertTrue(sourceManager.getSourceMapEntries(max).isEmpty()); + + program.undo(); + assertEquals(1, sourceManager.getSourceMapEntries(min).size()); + assertEquals(1, sourceManager.getSourceMapEntries(max).size()); + + program.redo(); + assertTrue(sourceManager.getSourceMapEntries(min).isEmpty()); + assertTrue(sourceManager.getSourceMapEntries(max).isEmpty()); + } + + public void testRemovingRemovedWithLengthZero() throws AddressOverflowException, LockException { + SourceMapEntry entry = null; + + int txId = program.startTransaction("adding source map entries"); + try { + entry = sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), 0); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("removing source map entry"); + try { + assertTrue(sourceManager.removeSourceMapEntry(entry)); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("removing source map entry"); + try { + assertFalse(sourceManager.removeSourceMapEntry(entry)); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testAddingConflictingEntries() throws AddressOverflowException, LockException { + int txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), 2); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("adding conflicting source map info"); + try { + sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), 3); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testAddingOverlappingEntrySameSourceInfoBefore() + throws AddressOverflowException, LockException { + int txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, ret2_2.getAddress(), ret2_2.getLength()); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("adding overlapping source map info"); + try { + sourceManager.addSourceMapEntry(source1, 1, nop1_1.getAddress(), + nop1_1.getLength() + 1); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testAddingOverlappingEntryDifferentSourceInfoBefore() + throws AddressOverflowException, LockException { + int txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, ret2_2.getAddress(), ret2_2.getLength()); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("adding overlapping source map info"); + try { + sourceManager.addSourceMapEntry(source2, 2, nop1_1.getAddress(), + nop1_1.getLength() + 1); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testAddingOverlappingEntrySameSourceInfoAfter() + throws AddressOverflowException, LockException { + int txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), + ret2_1.getLength() + 1); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("adding overlapping source map info"); + try { + sourceManager.addSourceMapEntry(source1, 1, nop1_1.getAddress(), nop1_1.getLength()); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testAddingOverlappingEntryDifferentSourceInfoAfter() + throws AddressOverflowException, LockException { + int txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), + ret2_1.getLength() + 1); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("adding overlapping source map info"); + try { + sourceManager.addSourceMapEntry(source2, 2, nop1_1.getAddress(), nop1_1.getLength()); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testAdjacentEntries() throws LockException, AddressOverflowException { + int txId = program.startTransaction("setting source map info"); + SourceMapEntry entry1 = null; + SourceMapEntry entry2 = null; + SourceMapEntry entry3 = null; + SourceMapEntry entry4 = null; + SourceMapEntry entry5 = null; + try { + entry1 = sourceManager.addSourceMapEntry(source1, 1, getBody(ret2_2)); + entry2 = sourceManager.addSourceMapEntry(source1, 2, getBody(nop1_2)); + entry3 = sourceManager.addSourceMapEntry(source2, 3, ret2_2.getAddress(), 0); + entry4 = sourceManager.addSourceMapEntry(source3, 4, ret2_2.getAddress().add(1), 0); + entry5 = sourceManager.addSourceMapEntry(source2, 5, nop1_2.getAddress(), 0); + } + finally { + program.endTransaction(txId, true); + } + + List entries = sourceManager.getSourceMapEntries(ret2_2.getAddress()); + assertEquals(2, entries.size()); + assertTrue(entries.contains(entry1)); + assertTrue(entries.contains(entry3)); + + entries = sourceManager.getSourceMapEntries(ret2_2.getAddress().add(1)); + assertEquals(2, entries.size()); + assertTrue(entries.contains(entry1)); + assertTrue(entries.contains(entry4)); + + assertEquals(ret2_2.getAddress().add(2), nop1_2.getAddress()); + entries = sourceManager.getSourceMapEntries(nop1_2.getAddress()); + assertTrue(entries.contains(entry2)); + assertTrue(entries.contains(entry5)); + + entries = sourceManager.getSourceMapEntries(ret2_2.getAddress().subtract(1)); + assertEquals(0, entries.size()); + + entries = sourceManager.getSourceMapEntries(nop1_2.getAddress().add(1)); + assertEquals(0, entries.size()); + } + + @Test + public void testClearingAllEntriesForOneSourceFile() throws LockException { + + int txId = program.startTransaction("setting source map info"); + SourceMapEntry source1Entry = null; + SourceMapEntry source3Entry = null; + try { + source1Entry = sourceManager.addSourceMapEntry(source1, 1, getBody(ret2_1)); + sourceManager.addSourceMapEntry(source2, 2, getBody(ret2_2)); + source3Entry = sourceManager.addSourceMapEntry(source3, 3, getBody(ret2_3)); + } + finally { + program.endTransaction(txId, true); + } + + List sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(3, sourceFiles.size()); + + List source2Entries = + sourceManager.getSourceMapEntries(source2, 0, Integer.MAX_VALUE); + assertEquals(1, source2Entries.size()); + List source2EntriesRestricted = + sourceManager.getSourceMapEntries(source2, 2, 2); + assertEquals(1, source2EntriesRestricted.size()); + assertEquals(source2Entries.get(0), source2EntriesRestricted.get(0)); + assertTrue(sourceManager.getSourceMapEntries(source1, 4, 5).isEmpty()); + + txId = program.startTransaction("clearing source info for source2"); + try { + for (SourceMapEntry entry : source2Entries) { + assertTrue(sourceManager.removeSourceMapEntry(entry)); + } + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(2, sourceFiles.size()); + assertFalse(sourceFiles.contains(source2)); + + assertEquals(1, sourceManager.getSourceMapEntries(ret2_1.getAddress()).size()); + assertEquals(source1Entry, sourceManager.getSourceMapEntries(ret2_1.getAddress()).get(0)); + assertTrue(sourceManager.getSourceMapEntries(ret2_2.getAddress()).isEmpty()); + assertEquals(1, sourceManager.getSourceMapEntries(ret2_3.getAddress()).size()); + assertEquals(source3Entry, sourceManager.getSourceMapEntries(ret2_3.getAddress()).get(0)); + } + + @Test + public void testDeletingSourceFile() throws LockException, IOException { + + SourceMapEntry entry2 = null; + int txId = program.startTransaction("setting source map info"); + try { + sourceManager.addSourceMapEntry(source1, 1, getBody(ret2_1)); + entry2 = sourceManager.addSourceMapEntry(source2, 2, getBody(ret2_2)); + sourceManager.addSourceMapEntry(source1, 3, getBody(ret2_1)); + } + finally { + program.endTransaction(txId, true); + } + + List sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(2, sourceFiles.size()); + assertTrue(sourceFiles.contains(source1)); + assertTrue(sourceFiles.contains(source2)); + + List source2Entries = + sourceManager.getSourceMapEntries(ret2_2.getAddress()); + assertEquals(1, source2Entries.size()); + assertEquals(entry2, source2Entries.get(0)); + + txId = program.startTransaction("deleting source file"); + try { + assertTrue(sourceManager.removeSourceFile(source2)); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(1, sourceFiles.size()); + SourceFile mappedFile = sourceFiles.get(0); + assertEquals(source1, mappedFile); + assertTrue(sourceManager.getSourceMapEntries(ret2_2.getAddress()).isEmpty()); + + // undo delete and verify that everything is restored + program.undo(); + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(2, sourceFiles.size()); + assertTrue(sourceFiles.contains(source1)); + assertTrue(sourceFiles.contains(source2)); + + assertEquals(1, sourceManager.getSourceMapEntries(ret2_2.getAddress()).size()); + SourceMapEntry sourceInfo = sourceManager.getSourceMapEntries(ret2_2.getAddress()).get(0); + assertEquals(2, sourceInfo.getLineNumber()); + assertEquals(source2, sourceInfo.getSourceFile()); + + // redo delete and verify + program.redo(); + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(1, sourceFiles.size()); + assertEquals(source1, sourceFiles.get(0)); + assertTrue(sourceManager.getSourceMapEntries(ret2_2.getAddress()).isEmpty()); + } + + @Test + public void testTransferringSourceMapInfo() throws LockException { + assertTrue(sourceManager.getMappedSourceFiles().isEmpty()); + + AddressRange both = new AddressRangeImpl(ret2_1.getMinAddress(), ret2_2.getMaxAddress()); + + int txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, both); + } + finally { + program.endTransaction(txId, true); + } + + List sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(1, sourceFiles.size()); + assertEquals(source1, sourceFiles.get(0)); + assertEquals(1, sourceManager.getSourceMapEntries(source1).size()); + assertTrue(sourceManager.getSourceMapEntries(source2).isEmpty()); + + // test redundant transfer + txId = program.startTransaction("transferring source map entries"); + try { + sourceManager.transferSourceMapEntries(source1, new SourceFile(path1)); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(1, sourceFiles.size()); + assertEquals(source1, sourceFiles.get(0)); + assertEquals(1, sourceManager.getSourceMapEntries(source1).size()); + assertTrue(sourceManager.getSourceMapEntries(source2).isEmpty()); + + txId = program.startTransaction("transferring source map entries"); + try { + sourceManager.transferSourceMapEntries(source1, source2); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(1, sourceFiles.size()); + assertEquals(source2, sourceFiles.get(0)); + assertTrue(sourceManager.getSourceMapEntries(source1).isEmpty()); + assertEquals(1, sourceManager.getSourceMapEntries(source2).size()); + + List entries = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(1, entries.size()); + SourceMapEntry entry = entries.get(0); + assertEquals(source2, entry.getSourceFile()); + assertEquals(1, entry.getLineNumber()); + assertEquals(both, entry.getRange()); + + // test transfer of files with no source map entries + assertEquals(0, sourceManager.getSourceMapEntries(source1).size()); + assertEquals(1, sourceManager.getSourceMapEntries(source2).size()); + assertEquals(0, sourceManager.getSourceMapEntries(source3).size()); + txId = program.startTransaction("transferring source map entries"); + try { + sourceManager.transferSourceMapEntries(source3, source1); + } + finally { + program.endTransaction(txId, true); + } + assertEquals(0, sourceManager.getSourceMapEntries(source1).size()); + assertEquals(1, sourceManager.getSourceMapEntries(source2).size()); + assertEquals(0, sourceManager.getSourceMapEntries(source3).size()); + } + + @Test + public void testIntersectsSourceFileEntry() throws LockException, AddressOverflowException { + assertTrue(sourceManager.getMappedSourceFiles().isEmpty()); + AddressSet everything = program.getAddressFactory().getAddressSet(); + assertFalse(sourceManager.intersectsSourceMapEntry(everything)); + assertFalse(sourceManager.intersectsSourceMapEntry(null)); + assertFalse(sourceManager.intersectsSourceMapEntry(new AddressSet())); + assertFalse(sourceManager.intersectsSourceMapEntry(new AddressSet(Address.NO_ADDRESS))); + + int txId = program.startTransaction("setting source map info"); + try { + sourceManager.addSourceMapEntry(source2, 2, ret2_2.getAddress(), 0); + sourceManager.addSourceMapEntry(source3, 3, getBody(ret2_3)); + } + finally { + program.endTransaction(txId, true); + } + + assertTrue(sourceManager.intersectsSourceMapEntry(everything)); + assertFalse(sourceManager.intersectsSourceMapEntry(new AddressSet(Address.NO_ADDRESS))); + assertFalse(sourceManager.intersectsSourceMapEntry(null)); + assertFalse(sourceManager.intersectsSourceMapEntry(new AddressSet())); + + // test with a length 0 entry + assertFalse(sourceManager + .intersectsSourceMapEntry(new AddressSet(ret2_2.getAddress().subtract(1)))); + assertTrue(sourceManager.intersectsSourceMapEntry(new AddressSet(ret2_2.getAddress()))); + assertFalse( + sourceManager.intersectsSourceMapEntry(new AddressSet(ret2_2.getAddress().add(1)))); + + assertFalse(sourceManager + .intersectsSourceMapEntry(new AddressSet(ret2_3.getAddress().subtract(1)))); + assertTrue(sourceManager.intersectsSourceMapEntry(new AddressSet(ret2_3.getAddress()))); + assertTrue( + sourceManager.intersectsSourceMapEntry(new AddressSet(ret2_3.getAddress().add(1)))); + assertFalse( + sourceManager.intersectsSourceMapEntry(new AddressSet(ret2_3.getAddress().add(2)))); + + assertTrue(sourceManager.intersectsSourceMapEntry(new AddressSet(getBody(ret2_3)))); + + } + + @Test + public void testSourceLineBounds() throws AddressOverflowException, LockException { + assertNull(SourceFileUtils.getSourceLineBounds(program, source1)); + assertNull(SourceFileUtils.getSourceLineBounds(program, source2)); + assertNull(SourceFileUtils.getSourceLineBounds(program, source3)); + + int txId = program.startTransaction("Adding source map entries"); + try { + sourceManager.addSourceMapEntry(source1, 10, ret2_1.getAddress(), 4); + } + finally { + program.endTransaction(txId, true); + } + + SourceLineBounds bounds = SourceFileUtils.getSourceLineBounds(program, source1); + assertEquals(10, bounds.min()); + assertEquals(10, bounds.max()); + assertNull(SourceFileUtils.getSourceLineBounds(program, source2)); + assertNull(SourceFileUtils.getSourceLineBounds(program, source3)); + + txId = program.startTransaction("Adding source map entries"); + try { + sourceManager.addSourceMapEntry(source1, 5, ret2_1.getAddress(), 4); + } + finally { + program.endTransaction(txId, true); + } + + bounds = SourceFileUtils.getSourceLineBounds(program, source1); + assertEquals(5, bounds.min()); + assertEquals(10, bounds.max()); + assertNull(SourceFileUtils.getSourceLineBounds(program, source2)); + assertNull(SourceFileUtils.getSourceLineBounds(program, source3)); + + txId = program.startTransaction("Adding source map entries"); + try { + sourceManager.addSourceMapEntry(source1, 20, ret2_3.getAddress(), 0); + } + finally { + program.endTransaction(txId, true); + } + + bounds = SourceFileUtils.getSourceLineBounds(program, source1); + assertEquals(5, bounds.min()); + assertEquals(20, bounds.max()); + assertNull(SourceFileUtils.getSourceLineBounds(program, source2)); + assertNull(SourceFileUtils.getSourceLineBounds(program, source3)); + } + + @Test + public void testUniquenessAfterTransfer() throws AddressOverflowException, LockException { + int txId = program.startTransaction("Adding source map entries"); + try { + sourceManager.addSourceMapEntry(source1, 10, ret2_1.getAddress(), 4); + sourceManager.addSourceMapEntry(source2, 10, ret2_1.getAddress(), 4); + } + finally { + program.endTransaction(txId, true); + } + assertEquals(2, sourceManager.getSourceMapEntries(ret2_1.getAddress()).size()); + + txId = program.startTransaction("transferring source map entries"); + try { + sourceManager.transferSourceMapEntries(source1, source2); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(1, sourceManager.getSourceMapEntries(ret2_1.getAddress()).size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testAddingEntryBeforeAddingFile() throws AddressOverflowException, LockException { + SourceFile sourceFile = new SourceFile("/src/test/file123.cc"); + int txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(sourceFile, 1, ret2_1.getAddress(), 1); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testTransferringSourceFileInfoSourceNotAdded() throws LockException { + SourceFile sourceFile = new SourceFile("/src/test/file123.cc"); + int txId = program.startTransaction("transferring source map entries"); + try { + sourceManager.transferSourceMapEntries(sourceFile, source1); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testTransferringSourceFileInfoTargetNotAdded() throws LockException { + SourceFile sourceFile = new SourceFile("/src/test/file123.cc"); + int txId = program.startTransaction("transferring source map entries"); + try { + sourceManager.transferSourceMapEntries(source1, sourceFile); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testMappingSamePathDifferentIdentifiers() + throws AddressOverflowException, LockException { + HexFormat hexFormat = HexFormat.of(); + assertEquals(3, sourceManager.getAllSourceFiles().size()); + String path = "/src/test/file.c"; + SourceFile test1 = new SourceFile(path); + SourceFile test2 = new SourceFile(path, SourceFileIdType.MD5, + hexFormat.parseHex("0123456789abcdef0123456789abcdef")); + SourceFile test3 = + new SourceFile(path, SourceFileIdType.TIMESTAMP_64, Longs.toByteArray(0)); + SourceMapEntry entry1 = null; + SourceMapEntry entry2 = null; + SourceMapEntry entry3 = null; + + List sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(0, sourceFiles.size()); + + List entries = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(0, entries.size()); + + int txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceFile(test1); + entry1 = sourceManager.addSourceMapEntry(test1, 1, ret2_1.getAddress(), 2); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(1, sourceFiles.size()); + assertEquals(test1, sourceFiles.get(0)); + + assertEquals(1, sourceManager.getSourceMapEntries(test1).size()); + assertTrue(sourceManager.getSourceMapEntries(test2).isEmpty()); + assertTrue(sourceManager.getSourceMapEntries(test3).isEmpty()); + + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(1, entries.size()); + assertEquals(entry1, entries.get(0)); + + txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceFile(test2); + entry2 = sourceManager.addSourceMapEntry(test2, 2, ret2_1.getAddress(), 2); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(2, sourceFiles.size()); + assertTrue(sourceFiles.contains(test1)); + assertTrue(sourceFiles.contains(test2)); + + assertEquals(1, sourceManager.getSourceMapEntries(test1).size()); + assertEquals(1, sourceManager.getSourceMapEntries(test2).size()); + assertTrue(sourceManager.getSourceMapEntries(test3).isEmpty()); + + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(2, entries.size()); + assertTrue(entries.contains(entry1)); + assertTrue(entries.contains(entry2)); + + txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceFile(test3); + entry3 = sourceManager.addSourceMapEntry(test3, 3, ret2_1.getAddress(), 2); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(3, sourceFiles.size()); + assertTrue(sourceFiles.contains(test1)); + assertTrue(sourceFiles.contains(test2)); + assertTrue(sourceFiles.contains(test3)); + + assertEquals(1, sourceManager.getSourceMapEntries(test1).size()); + assertEquals(1, sourceManager.getSourceMapEntries(test2).size()); + assertEquals(1, sourceManager.getSourceMapEntries(test3).size()); + + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(3, entries.size()); + assertTrue(entries.contains(entry1)); + assertTrue(entries.contains(entry2)); + assertTrue(entries.contains(entry3)); + + txId = program.startTransaction("removing source map entry"); + try { + assertTrue(sourceManager.removeSourceMapEntry(entry3)); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(2, sourceFiles.size()); + assertTrue(sourceFiles.contains(test1)); + assertTrue(sourceFiles.contains(test2)); + + assertEquals(1, sourceManager.getSourceMapEntries(test1).size()); + assertEquals(1, sourceManager.getSourceMapEntries(test2).size()); + assertTrue(sourceManager.getSourceMapEntries(test3).isEmpty()); + + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(2, entries.size()); + assertTrue(entries.contains(entry1)); + assertTrue(entries.contains(entry2)); + + txId = program.startTransaction("removing source file"); + try { + assertTrue(sourceManager.removeSourceFile(test2)); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getMappedSourceFiles(); + assertEquals(1, sourceFiles.size()); + assertTrue(sourceFiles.contains(test1)); + + assertEquals(1, sourceManager.getSourceMapEntries(test1).size()); + assertTrue(sourceManager.getSourceMapEntries(test2).isEmpty()); + assertTrue(sourceManager.getSourceMapEntries(test3).isEmpty()); + + entries = sourceManager.getSourceMapEntries(ret2_1.getAddress()); + assertEquals(1, entries.size()); + assertTrue(entries.contains(entry1)); + } + + @Test(expected = AddressOutOfBoundsException.class) + public void testAddingEntryAfterBlocks() + throws AddressOverflowException, LockException { + MemoryBlock[] blocks = program.getMemory().getBlocks(); + assertEquals(1, blocks.length); + int txId = program.startTransaction("adding entry after block"); + try { + sourceManager.addSourceMapEntry(source1, 1, blocks[0].getEnd().add(0x10), 1); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = AddressOutOfBoundsException.class) + public void testAddingLengthZeroEntryAfterBlocks() + throws AddressOverflowException, LockException { + MemoryBlock[] blocks = program.getMemory().getBlocks(); + assertEquals(1, blocks.length); + int txId = program.startTransaction("adding length zero entry after block"); + try { + sourceManager.addSourceMapEntry(source1, 1, blocks[0].getEnd().add(0x10), 0); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = AddressOutOfBoundsException.class) + public void testAddingEntryOverlappingEndOfBlock() + throws AddressOverflowException, LockException { + MemoryBlock[] blocks = program.getMemory().getBlocks(); + assertEquals(1, blocks.length); + int txId = program.startTransaction("adding entry overlapping block end"); + try { + sourceManager.addSourceMapEntry(source1, 1, blocks[0].getEnd().subtract(5),10); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = AddressOutOfBoundsException.class) + public void testAddingEntryBeforeBlocks() + throws AddressOverflowException, LockException { + MemoryBlock[] blocks = program.getMemory().getBlocks(); + assertEquals(1, blocks.length); + int txId = program.startTransaction("adding entry after block"); + try { + sourceManager.addSourceMapEntry(source1, 1, blocks[0].getStart().subtract(0x10), 1); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = AddressOutOfBoundsException.class) + public void testAddingLengthZeroEntryBeforeBlocks() + throws AddressOverflowException, LockException { + MemoryBlock[] blocks = program.getMemory().getBlocks(); + assertEquals(1, blocks.length); + int txId = program.startTransaction("adding length zero entry after block"); + try { + sourceManager.addSourceMapEntry(source1, 1, blocks[0].getStart().subtract(0x10), 0); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = AddressOutOfBoundsException.class) + public void testAddingEntryOverlappingStartOfBlock() + throws AddressOverflowException, LockException { + MemoryBlock[] blocks = program.getMemory().getBlocks(); + assertEquals(1, blocks.length); + int txId = program.startTransaction("adding entry overlapping block end"); + try { + sourceManager.addSourceMapEntry(source1, 1, blocks[0].getStart().subtract(5), 10); + } + finally { + program.endTransaction(txId, true); + } + } + + + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/ModifyingMemoryMappingSourceFilesTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/ModifyingMemoryMappingSourceFilesTest.java new file mode 100644 index 0000000000..4ff9ecf638 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/ModifyingMemoryMappingSourceFilesTest.java @@ -0,0 +1,1207 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database.sourcemap; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.framework.store.LockException; +import ghidra.program.model.address.*; +import ghidra.program.model.mem.*; +import ghidra.program.model.sourcemap.SourceMapEntry; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.NotFoundException; +import ghidra.util.task.TaskMonitor; + +public class ModifyingMemoryMappingSourceFilesTest extends AbstractSourceFileTest { + + protected SourceFileManagerDB sourceDB; + private MemoryBlock textBlock; + private Address blockStart; + private Memory memory; + + @Before + public void init() { + sourceDB = (SourceFileManagerDB) sourceManager; + textBlock = program.getMemory().getBlock(".text"); + blockStart = textBlock.getStart(); + memory = program.getMemory(); + } + + @Test(expected = AddressOverflowException.class) + public void testAddingEntryWrappingSpace() throws LockException, IllegalArgumentException, + MemoryConflictException, AddressOverflowException, CancelledException { + Address spaceMin = blockStart.getAddressSpace().getMinAddress(); + Address spaceMax = blockStart.getAddressSpace().getMaxAddress(); + int txId = program.startTransaction("initializing memory"); + try { + memory.createInitializedBlock("min", spaceMin, 10L, (byte) 0, TaskMonitor.DUMMY, false); + memory.createInitializedBlock("max", spaceMax.subtract(10), 10, (byte) 0, + TaskMonitor.DUMMY, false); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("adding wrapping entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, spaceMax.subtract(5), 10); + } + finally { + program.endTransaction(txId, true); + } + + } + + @Test(expected = AddressOutOfBoundsException.class) + public void testAddingEntryCompletelyOutsideBlock() + throws AddressOverflowException, LockException { + Address start = textBlock.getStart().subtract(10); + assertFalse(memory.intersects(start.subtract(10), start.subtract(5))); + + int txId = program.startTransaction("adding entry outside block"); + try { + sourceManager.addSourceMapEntry(source1, 1, start.subtract(10), 5); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = AddressOutOfBoundsException.class) + public void testAddingLengthZeroEntryOutsideBlock() + throws AddressOverflowException, LockException { + Address start = textBlock.getStart().subtract(10); + assertFalse(memory.contains(start.subtract(10))); + + int txId = program.startTransaction("adding length 0 entry outside block"); + try { + sourceManager.addSourceMapEntry(source1, 1, start.subtract(10), 0); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = AddressOutOfBoundsException.class) + public void testAddingEntryUndefinedStart() throws AddressOverflowException, LockException { + Address start = textBlock.getStart().subtract(5); + assertFalse(memory.contains(start)); + + int txId = program.startTransaction("adding entry with undefined start"); + try { + sourceManager.addSourceMapEntry(source1, 1, start, 10); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = AddressOutOfBoundsException.class) + public void testAddingEntryUndefinedEnd() throws LockException { + Address end = textBlock.getEnd().add(5); + assertFalse(memory.contains(end)); + + int txId = program.startTransaction("adding entry with undefined end"); + try { + sourceManager.addSourceMapEntry(source1, 1, + new AddressRangeImpl(textBlock.getEnd().subtract(5), end)); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = AddressOutOfBoundsException.class) + public void testAddingEntrySpanningUndefinedRange() + throws MemoryBlockException, LockException, NotFoundException, + AddressOverflowException { + Address removeStart = blockStart.add(10); + Address removeEnd = blockStart.add(15); + assertTrue(memory.contains(removeStart, removeEnd)); + + int txId = program.startTransaction("removing range"); + try { + memory.split(textBlock, removeStart); + MemoryBlock blockToSplit = memory.getBlock(removeStart); + memory.split(blockToSplit, removeEnd.add(1)); + MemoryBlock blockToRemove = memory.getBlock(removeStart); + memory.removeBlock(blockToRemove, TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + assertFalse(memory.intersects(removeStart, removeEnd)); + + txId = program.startTransaction("adding entry containing removed range"); + try { + sourceManager.addSourceMapEntry(source1, 1, removeStart.subtract(5), 20); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testSetImageBase() throws AddressOverflowException, LockException { + blockStart = textBlock.getStart(); + + int txId = program.startTransaction("adding entries"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart, 0x10); + sourceManager.addSourceMapEntry(source2, 2, blockStart.add(0x20), 0); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("Setting image base"); + try { + program.setImageBase(program.getImageBase().add(0x200000), true); + } + finally { + program.endTransaction(txId, true); + } + + List entries = sourceManager.getSourceMapEntries(source1); + assertEquals(1, entries.size()); + SourceMapEntry entry = entries.get(0); + assertEquals(source1, entry.getSourceFile()); + assertEquals(1, entry.getLineNumber()); + assertEquals(blockStart.add(0x200000), entry.getBaseAddress()); + assertEquals(0x10, entry.getLength()); + + entries = sourceManager.getSourceMapEntries(source2); + assertEquals(1, entries.size()); + entry = entries.get(0); + assertEquals(source2, entry.getSourceFile()); + assertEquals(2, entry.getLineNumber()); + assertEquals(blockStart.add(0x200000).add(0x20), entry.getBaseAddress()); + assertEquals(0, entry.getLength()); + } + + @Test + public void testAddingEntrySpanningAdjacentBlocks() throws MemoryBlockException, LockException, + NotFoundException, AddressOutOfBoundsException, AddressOverflowException { + blockStart = textBlock.getStart(); + + int txId = program.startTransaction("splitting block"); + try { + program.getMemory().split(textBlock, blockStart.add(20)); + } + finally { + program.endTransaction(txId, true); + } + + MemoryBlock leftBlock = program.getMemory().getBlock(blockStart); + txId = program.startTransaction("splitting left block"); + try { + program.getMemory().split(leftBlock, blockStart.add(10)); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart.add(5), 40); + } + finally { + program.endTransaction(txId, true); + } + + List entries = sourceManager.getSourceMapEntries(source1); + assertEquals(1, entries.size()); + SourceMapEntry entry = entries.get(0); + assertEquals(source1, entry.getSourceFile()); + assertEquals(1, entry.getLineNumber()); + assertEquals(blockStart.add(5), entry.getBaseAddress()); + assertEquals(40, entry.getLength()); + } + + @Test + public void testSplittingBlockContainingEntry() + throws AddressOverflowException, LockException, MemoryBlockException, NotFoundException, + AddressOutOfBoundsException { + + int txId = program.startTransaction("adding source map entry"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart.add(5), 20); + } + finally { + program.endTransaction(txId, true); + } + + txId = program.startTransaction("splitting block"); + try { + program.getMemory().split(textBlock, blockStart.add(10)); + } + finally { + program.endTransaction(txId, true); + } + + List entries = sourceManager.getSourceMapEntries(source1); + assertEquals(1, entries.size()); + SourceMapEntry entry = entries.get(0); + assertEquals(source1, entry.getSourceFile()); + assertEquals(1, entry.getLineNumber()); + assertEquals(blockStart.add(5), entry.getBaseAddress()); + assertEquals(20, entry.getLength()); + } + + private void setUpNoEntriesInRegionTest() + throws AddressOverflowException, LockException, AddressOutOfBoundsException { + int txId = program.startTransaction("adding source map entries"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart.add(5), 3); + sourceManager.addSourceMapEntry(source2, 2, blockStart.add(5), 3); + sourceManager.addSourceMapEntry(source3, 3, blockStart.add(7), 0); + sourceManager.addSourceMapEntry(source1, 11, blockStart.add(25), 6); + sourceManager.addSourceMapEntry(source2, 22, blockStart.add(25), 6); + sourceManager.addSourceMapEntry(source3, 33, blockStart.add(27), 0); + } + finally { + program.endTransaction(txId, true); + } + } + + private void checkNoEntriesInRegion() { + + assertEquals(2, sourceManager.getSourceMapEntries(source1).size()); + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 3); + + entries = sourceManager.getSourceMapEntries(source1, 11); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(25), 6); + + assertEquals(2, sourceManager.getSourceMapEntries(source2).size()); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 3); + + entries = sourceManager.getSourceMapEntries(source2, 22); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(25), 6); + + assertEquals(2, sourceManager.getSourceMapEntries(source3).size()); + + entries = sourceManager.getSourceMapEntries(source3, 3); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(7), 0); + + entries = sourceManager.getSourceMapEntries(source3, 33); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(27), 0); + } + + private void checkBaseAddressAndLength(SourceMapEntry entry, Address baseAddr, long length) { + assertEquals(baseAddr, entry.getBaseAddress()); + assertEquals(length, entry.getLength()); + } + + + @Test + public void testMovingRegionWithNoEntries() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpNoEntriesInRegionTest(); + int txId = program.startTransaction("moving region"); + try { + sourceDB.moveAddressRange(blockStart.add(10), blockStart.add(0x50000), 10, + TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + checkNoEntriesInRegion(); + } + + @Test + public void testDeletingRegionWithNoEntries() throws AddressOverflowException, LockException, + AddressOutOfBoundsException, CancelledException { + setUpNoEntriesInRegionTest(); + int txId = program.startTransaction("deleting region"); + try { + sourceDB.deleteAddressRange(blockStart.add(10), blockStart.add(20), TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + checkNoEntriesInRegion(); + } + + private void setUpRegionContainingEntries() + throws AddressOverflowException, LockException, AddressOutOfBoundsException { + + int txId = program.startTransaction("adding entries"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart.add(10 + 1), 2); + sourceManager.addSourceMapEntry(source2, 2, blockStart.add(10 + 1), 2); + sourceManager.addSourceMapEntry(source3, 3, blockStart.add(10 + 2), 0); + sourceManager.addSourceMapEntry(source1, 11, blockStart.add(10 + 6), 3); + sourceManager.addSourceMapEntry(source2, 22, blockStart.add(10 + 6), 3); + sourceManager.addSourceMapEntry(source3, 33, blockStart.add(10 + 7), 0); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testMovingRegionContainingEntries() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpRegionContainingEntries(); + + int txId = program.startTransaction("moving region"); + try { + sourceDB.moveAddressRange(blockStart.add(10), blockStart.add(0x100000), 10, + TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(2, sourceManager.getSourceMapEntries(source1).size()); + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(0x100000 + 1), 2); + + entries = sourceManager.getSourceMapEntries(source1, 11); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(0x100000 + 6), 3); + + assertEquals(2, sourceManager.getSourceMapEntries(source2).size()); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(0x100000 + 1), 2); + + entries = sourceManager.getSourceMapEntries(source2, 22); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(0x100000 + 6), 3); + + assertEquals(2, sourceManager.getSourceMapEntries(source3).size()); + + entries = sourceManager.getSourceMapEntries(source3, 3); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(0x100000 + 2), 0); + + entries = sourceManager.getSourceMapEntries(source3, 33); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(0x100000 + 7), 0); + + } + + @Test + public void testDeletingRegionContainingEntries() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpRegionContainingEntries(); + + int txId = program.startTransaction("deleting region"); + try { + sourceDB.deleteAddressRange(blockStart.add(10), blockStart.add(20), TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(0, sourceManager.getMappedSourceFiles().size()); + assertEquals(0, sourceManager.getSourceMapEntries(source1).size()); + assertEquals(0, sourceManager.getSourceMapEntries(source2).size()); + assertEquals(0, sourceManager.getSourceMapEntries(source3).size()); + } + + private void setUpOverlappingStart() + throws AddressOverflowException, LockException, AddressOutOfBoundsException { + int txId = program.startTransaction("adding entries"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart.add(5), 10); + sourceManager.addSourceMapEntry(source1, 11, blockStart.add(5), 10); + sourceManager.addSourceMapEntry(source2, 2, blockStart.add(5), 10); + sourceManager.addSourceMapEntry(source2, 22, blockStart.add(5), 10); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testMovingRegionWithEntryOverlappingStart() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpOverlappingStart(); + int txId = program.startTransaction("moving region"); + try { + sourceDB.moveAddressRange(blockStart.add(10), blockStart.add(0x100000), 10, + TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100000), 5); + + entries = sourceManager.getSourceMapEntries(source1, 11); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100000), 5); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100000), 5); + + entries = sourceManager.getSourceMapEntries(source2, 22); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100000), 5); + + } + + @Test + public void testDeletingRegionWithEntryOverlappingStart() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpOverlappingStart(); + int txId = program.startTransaction("deleting region"); + try { + sourceDB.deleteAddressRange(blockStart.add(10), blockStart.add(100), TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + + entries = sourceManager.getSourceMapEntries(source1, 11); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + + entries = sourceManager.getSourceMapEntries(source2, 22); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + + } + + private void setUpOverlappingEnd() + throws AddressOverflowException, LockException, AddressOutOfBoundsException { + int txId = program.startTransaction("adding entries"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart.add(15), 10); + sourceManager.addSourceMapEntry(source1, 11, blockStart.add(15), 10); + sourceManager.addSourceMapEntry(source2, 2, blockStart.add(15), 10); + sourceManager.addSourceMapEntry(source2, 22, blockStart.add(15), 10); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testMovingRegionWithEntryOverlappingEnd() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpOverlappingEnd(); + + int txId = program.startTransaction("moving region"); + try { + sourceDB.moveAddressRange(blockStart.add(10), blockStart.add(0x100000), 10, + TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(20), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100005), 5); + + entries = sourceManager.getSourceMapEntries(source1, 11); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(20), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100005), 5); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(20), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100005), 5); + + entries = sourceManager.getSourceMapEntries(source2, 22); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(20), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100005), 5); + } + + @Test + public void testDeletingRegionWithEntryOverlappingEnd() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpOverlappingEnd(); + + int txId = program.startTransaction("deleting range"); + try { + sourceDB.deleteAddressRange(blockStart.add(10), blockStart.add(20), TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(21), 4); + + entries = sourceManager.getSourceMapEntries(source1, 11); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(21), 4); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(21), 4); + + entries = sourceManager.getSourceMapEntries(source2, 22); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(21), 4); + } + + private void setUpContainingRegion() + throws AddressOverflowException, LockException, AddressOutOfBoundsException { + int txId = program.startTransaction("adding entries"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart.add(5), 20); + sourceManager.addSourceMapEntry(source1, 11, blockStart.add(5), 20); + sourceManager.addSourceMapEntry(source2, 2, blockStart.add(5), 20); + sourceManager.addSourceMapEntry(source2, 22, blockStart.add(5), 20); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testMovingRegionContainedWithinEntry() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpContainingRegion(); + + int txId = program.startTransaction("moving region"); + try { + sourceDB.moveAddressRange(blockStart.add(10), blockStart.add(0x100000), 10, + TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(3, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(20), 5); + checkBaseAddressAndLength(entries.get(2), blockStart.add(0x100000), 10); + + entries = sourceManager.getSourceMapEntries(source1, 11); + assertEquals(3, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(20), 5); + checkBaseAddressAndLength(entries.get(2), blockStart.add(0x100000), 10); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(3, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(20), 5); + checkBaseAddressAndLength(entries.get(2), blockStart.add(0x100000), 10); + + entries = sourceManager.getSourceMapEntries(source2, 22); + assertEquals(3, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(20), 5); + checkBaseAddressAndLength(entries.get(2), blockStart.add(0x100000), 10); + } + + @Test + public void testDeletingRegionContainedWithinEntry() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpContainingRegion(); + + int txId = program.startTransaction("deleting region"); + try { + sourceDB.deleteAddressRange(blockStart.add(10), blockStart.add(20), TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(21), 4); + + entries = sourceManager.getSourceMapEntries(source1, 11); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(21), 4); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(21), 4); + + entries = sourceManager.getSourceMapEntries(source2, 22); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(5), 5); + checkBaseAddressAndLength(entries.get(1), blockStart.add(21), 4); + } + + private void setUpLengthZeroEntries() + throws AddressOverflowException, LockException, AddressOutOfBoundsException { + int txId = program.startTransaction("adding entries"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart.add(9), 0); + sourceManager.addSourceMapEntry(source2, 2, blockStart.add(9), 0); + sourceManager.addSourceMapEntry(source3, 3, blockStart.add(9), 0); + sourceManager.addSourceMapEntry(source1, 11, blockStart.add(10), 0); + sourceManager.addSourceMapEntry(source2, 22, blockStart.add(10), 0); + sourceManager.addSourceMapEntry(source3, 33, blockStart.add(10), 0); + sourceManager.addSourceMapEntry(source1, 111, blockStart.add(11), 0); + sourceManager.addSourceMapEntry(source2, 222, blockStart.add(11), 0); + sourceManager.addSourceMapEntry(source3, 333, blockStart.add(11), 0); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testMovingOneAddressLengthZeroEntries() + throws AddressOverflowException, CancelledException, AddressOutOfBoundsException, + LockException { + setUpLengthZeroEntries(); + int txId = program.startTransaction("moving address"); + try { + sourceDB.moveAddressRange(blockStart.add(10), blockStart.add(0x100000), 1, + TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + assertEquals(3, sourceManager.getMappedSourceFiles().size()); + + assertEquals(3, sourceManager.getSourceMapEntries(source1).size()); + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 0); + + entries = sourceManager.getSourceMapEntries(source1, 11); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(0x100000), 0); + + entries = sourceManager.getSourceMapEntries(source1, 111); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 0); + + assertEquals(3, sourceManager.getSourceMapEntries(source2).size()); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 0); + + entries = sourceManager.getSourceMapEntries(source2, 22); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(0x100000), 0); + + entries = sourceManager.getSourceMapEntries(source2, 222); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 0); + + assertEquals(3, sourceManager.getSourceMapEntries(source3).size()); + + entries = sourceManager.getSourceMapEntries(source3, 3); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 0); + + entries = sourceManager.getSourceMapEntries(source3, 33); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(0x100000), 0); + + entries = sourceManager.getSourceMapEntries(source3, 333); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 0); + } + + @Test + public void testDeletingOneAddressLengthZeroEntries() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpLengthZeroEntries(); + int txId = program.startTransaction("deleting range"); + try { + sourceDB.deleteAddressRange(blockStart.add(10), blockStart.add(10), TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(3, sourceManager.getMappedSourceFiles().size()); + + assertEquals(2, sourceManager.getSourceMapEntries(source1).size()); + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 0); + + entries = sourceManager.getSourceMapEntries(source1, 111); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 0); + + assertEquals(2, sourceManager.getSourceMapEntries(source2).size()); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 0); + + entries = sourceManager.getSourceMapEntries(source2, 222); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 0); + + assertEquals(2, sourceManager.getSourceMapEntries(source3).size()); + + entries = sourceManager.getSourceMapEntries(source3, 3); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 0); + + entries = sourceManager.getSourceMapEntries(source3, 333); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 0); + } + + private void setUpLengthOneEntries() + throws AddressOverflowException, LockException, AddressOutOfBoundsException { + int txId = program.startTransaction("adding entries"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart.add(9), 1); + sourceManager.addSourceMapEntry(source2, 2, blockStart.add(9), 1); + sourceManager.addSourceMapEntry(source3, 3, blockStart.add(9), 1); + sourceManager.addSourceMapEntry(source1, 11, blockStart.add(10), 1); + sourceManager.addSourceMapEntry(source2, 22, blockStart.add(10), 1); + sourceManager.addSourceMapEntry(source3, 33, blockStart.add(10), 1); + sourceManager.addSourceMapEntry(source1, 111, blockStart.add(11), 1); + sourceManager.addSourceMapEntry(source2, 222, blockStart.add(11), 1); + sourceManager.addSourceMapEntry(source3, 333, blockStart.add(11), 1); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testMoveOneAddressLengthOneEntries() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpLengthOneEntries(); + + int txId = program.startTransaction("moving address"); + try { + sourceDB.moveAddressRange(blockStart.add(10), blockStart.add(0x100000), 1, + TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(3, sourceManager.getMappedSourceFiles().size()); + + assertEquals(3, sourceManager.getSourceMapEntries(source1).size()); + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 1); + + entries = sourceManager.getSourceMapEntries(source1, 11); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(0x100000), 1); + + entries = sourceManager.getSourceMapEntries(source1, 111); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 1); + + assertEquals(3, sourceManager.getSourceMapEntries(source2).size()); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 1); + + entries = sourceManager.getSourceMapEntries(source2, 22); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(0x100000), 1); + + entries = sourceManager.getSourceMapEntries(source2, 222); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 1); + + assertEquals(3, sourceManager.getSourceMapEntries(source3).size()); + + entries = sourceManager.getSourceMapEntries(source3, 3); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 1); + + entries = sourceManager.getSourceMapEntries(source3, 33); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(0x100000), 1); + + entries = sourceManager.getSourceMapEntries(source3, 333); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 1); + } + + @Test + public void testDeleteOneAddressLengthOneEntries() throws AddressOverflowException, + LockException, AddressOutOfBoundsException, CancelledException { + setUpLengthOneEntries(); + int txId = program.startTransaction("deleting range"); + try { + sourceDB.deleteAddressRange(blockStart.add(10), blockStart.add(10), TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(3, sourceManager.getMappedSourceFiles().size()); + + assertEquals(2, sourceManager.getSourceMapEntries(source1).size()); + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 1); + + entries = sourceManager.getSourceMapEntries(source1, 111); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 1); + + assertEquals(2, sourceManager.getSourceMapEntries(source2).size()); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 1); + + entries = sourceManager.getSourceMapEntries(source2, 222); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 1); + + assertEquals(2, sourceManager.getSourceMapEntries(source3).size()); + + entries = sourceManager.getSourceMapEntries(source3, 3); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 1); + + entries = sourceManager.getSourceMapEntries(source3, 333); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 1); + } + + private void setUpLengthTwoEntries1() + throws AddressOverflowException, LockException, AddressOutOfBoundsException { + int txId = program.startTransaction("adding entries"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart.add(9), 2); + sourceManager.addSourceMapEntry(source2, 2, blockStart.add(9), 2); + sourceManager.addSourceMapEntry(source3, 3, blockStart.add(9), 2); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testMoveOneAddressLengthTwoEntries1() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpLengthTwoEntries1(); + int txId = program.startTransaction("moving address"); + try { + sourceDB.moveAddressRange(blockStart.add(10), blockStart.add(0x100000), 1, + TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(3, sourceManager.getMappedSourceFiles().size()); + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 1); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100000), 1); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 1); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100000), 1); + + entries = sourceManager.getSourceMapEntries(source3, 3); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 1); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100000), 1); + } + + @Test + public void testDeletingOneAddressLength2Entries1() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpLengthTwoEntries1(); + int txId = program.startTransaction("deleting address"); + try { + sourceDB.deleteAddressRange(blockStart.add(10), blockStart.add(10), TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(3, sourceManager.getMappedSourceFiles().size()); + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 1); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 1); + + entries = sourceManager.getSourceMapEntries(source3, 3); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9), 1); + } + + private void setUpLengthTwoEntries2() + throws AddressOverflowException, LockException, AddressOutOfBoundsException { + int txId = program.startTransaction("adding entries"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart.add(10), 2); + sourceManager.addSourceMapEntry(source2, 2, blockStart.add(10), 2); + sourceManager.addSourceMapEntry(source3, 3, blockStart.add(10), 2); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testMoveOneAddressLengthTwoEntries2() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpLengthTwoEntries2(); + int txId = program.startTransaction("moving address"); + try { + sourceDB.moveAddressRange(blockStart.add(10), blockStart.add(0x100000), 1, + TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(3, sourceManager.getMappedSourceFiles().size()); + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 1); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100000), 1); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 1); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100000), 1); + + entries = sourceManager.getSourceMapEntries(source3, 3); + assertEquals(2, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 1); + checkBaseAddressAndLength(entries.get(1), blockStart.add(0x100000), 1); + } + + @Test + public void testDeletingOneAddressLength2Entries2() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpLengthTwoEntries2(); + int txId = program.startTransaction("deleting address"); + try { + sourceDB.deleteAddressRange(blockStart.add(10), blockStart.add(10), TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(3, sourceManager.getMappedSourceFiles().size()); + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 1); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 1); + + entries = sourceManager.getSourceMapEntries(source3, 3); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11), 1); + } + + private void setUpMovingRangeIntersection() + throws AddressOverflowException, LockException, AddressOutOfBoundsException { + int txId = program.startTransaction("adding new blentries"); + try { + sourceManager.addSourceMapEntry(source1, 1, blockStart.add(8), 2); + sourceManager.addSourceMapEntry(source2, 2, blockStart.add(8), 2); + sourceManager.addSourceMapEntry(source3, 3, blockStart.add(9), 0); + sourceManager.addSourceMapEntry(source1, 11, blockStart.add(10), 2); + sourceManager.addSourceMapEntry(source2, 22, blockStart.add(10), 2); + sourceManager.addSourceMapEntry(source3, 33, blockStart.add(11), 0); + sourceManager.addSourceMapEntry(source1, 111, blockStart.add(12), 2); + sourceManager.addSourceMapEntry(source2, 222, blockStart.add(12), 2); + sourceManager.addSourceMapEntry(source3, 333, blockStart.add(13), 0); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test + public void testMovingIntersectionRangesForward() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpMovingRangeIntersection(); + int txId = program.startTransaction("moving block"); + try { + sourceDB.moveAddressRange(blockStart, blockStart.add(3), textBlock.getSize(), + TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + assertEquals(3, sourceManager.getMappedSourceFiles().size()); + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(8 + 3), 2); + + entries = sourceManager.getSourceMapEntries(source1, 11); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(10 + 3), 2); + + entries = sourceManager.getSourceMapEntries(source1, 111); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(12 + 3), 2); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(8 + 3), 2); + + entries = sourceManager.getSourceMapEntries(source2, 22); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(10 + 3), 2); + + entries = sourceManager.getSourceMapEntries(source2, 222); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(12 + 3), 2); + + entries = sourceManager.getSourceMapEntries(source3, 3); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9 + 3), 0); + + entries = sourceManager.getSourceMapEntries(source3, 33); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11 + 3), 0); + + entries = sourceManager.getSourceMapEntries(source3, 333); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(13 + 3), 0); + } + + @Test + public void testMovingIntersectionRangesBackward() + throws AddressOverflowException, LockException, AddressOutOfBoundsException, + CancelledException { + setUpMovingRangeIntersection(); + int txId = program.startTransaction("moving block"); + try { + sourceDB.moveAddressRange(blockStart, blockStart.subtract(3), textBlock.getSize(), + TaskMonitor.DUMMY); + } + finally { + program.endTransaction(txId, true); + } + assertEquals(3, sourceManager.getMappedSourceFiles().size()); + + List entries = sourceManager.getSourceMapEntries(source1, 1); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(8 - 3), 2); + + entries = sourceManager.getSourceMapEntries(source1, 11); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(10 - 3), 2); + + entries = sourceManager.getSourceMapEntries(source1, 111); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(12 - 3), 2); + + entries = sourceManager.getSourceMapEntries(source2, 2); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(8 - 3), 2); + + entries = sourceManager.getSourceMapEntries(source2, 22); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(10 - 3), 2); + + entries = sourceManager.getSourceMapEntries(source2, 222); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(12 - 3), 2); + + entries = sourceManager.getSourceMapEntries(source3, 3); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(9 - 3), 0); + + entries = sourceManager.getSourceMapEntries(source3, 33); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(11 - 3), 0); + + entries = sourceManager.getSourceMapEntries(source3, 333); + assertEquals(1, entries.size()); + checkBaseAddressAndLength(entries.get(0), blockStart.add(13 - 3), 0); + + } + + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/SourceFileTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/SourceFileTest.java new file mode 100644 index 0000000000..23d3d011fb --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/SourceFileTest.java @@ -0,0 +1,508 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database.sourcemap; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; +import org.python.google.common.primitives.Longs; + +import ghidra.framework.store.LockException; +import ghidra.util.SourceFileUtils; + +public class SourceFileTest extends AbstractSourceFileTest { + + @Test(expected = IllegalArgumentException.class) + public void testNonFilePathFailure() { + new SourceFile("/test/dir/"); + } + + @Test(expected = IllegalArgumentException.class) + public void testRelativePathCreateFailure() { + new SourceFile("test1/test2.c"); + } + + @Test(expected = IllegalArgumentException.class) + public void testNullPathCreateFailure() { + new SourceFile((String) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyPathCreateFailure() { + new SourceFile(StringUtils.EMPTY); + } + + @Test + public void testPathNormalization() { + assertEquals("/src/dir1/dir2/file.c", + new SourceFile("/src/test/../dir1/test/../dir2/file.c").getPath()); + } + + @Test + public void testGetFilename() { + assertEquals("file.c", new SourceFile("/src/test/file.c").getFilename()); + } + + @Test + public void testUtilityMethod() { + SourceFile linux = SourceFileUtils.getSourceFileFromPathString("/src/test/../file1.c"); + assertEquals("/src/file1.c", linux.getPath()); + assertEquals("file1.c", linux.getFilename()); + + SourceFile windows = + SourceFileUtils.getSourceFileFromPathString("c:\\src\\test\\..\\file1.c"); + assertEquals("/c:/src/file1.c", windows.getPath()); + assertEquals("file1.c", windows.getFilename()); + + windows = + SourceFileUtils.getSourceFileFromPathString("/C:/Users//guest/./temp/../file.exe"); + assertEquals("/C:/Users/guest/file.exe", windows.getPath()); + assertEquals("file.exe", windows.getFilename()); + } + + @Test + public void basicAddRemoveTest() throws LockException { + assertEquals(3, sourceManager.getAllSourceFiles().size()); + SourceFile testSource = new SourceFile("/home/user/src/file.c"); + + int txId = program.startTransaction("adding source file"); + try { + assertTrue(sourceManager.addSourceFile(testSource)); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(4, sourceManager.getAllSourceFiles().size()); + + txId = program.startTransaction("adding same source file"); + try { + assertFalse(sourceManager.addSourceFile(testSource)); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(4, sourceManager.getAllSourceFiles().size()); + + txId = program.startTransaction("removing source file"); + try { + assertTrue(sourceManager.removeSourceFile(testSource)); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(3, sourceManager.getAllSourceFiles().size()); + + txId = program.startTransaction("removing source file again"); + try { + assertFalse(sourceManager.removeSourceFile(testSource)); + } + finally { + program.endTransaction(txId, true); + } + + assertEquals(3, sourceManager.getAllSourceFiles().size()); + } + + @Test + public void testContainsNull() { + assertFalse(sourceManager.containsSourceFile(null)); + } + + @Test + public void basicContainsTest() throws LockException, IOException { + assertTrue(sourceManager.containsSourceFile(source1)); + int txId = program.startTransaction("removing source1"); + try { + assertTrue(sourceManager.removeSourceFile(source1)); + } + finally { + program.endTransaction(txId, true); + } + assertFalse(sourceManager.containsSourceFile(source1)); + assertTrue(sourceManager.containsSourceFile(source2)); + assertTrue(sourceManager.containsSourceFile(source3)); + + program.undo(); + + assertTrue(sourceManager.containsSourceFile(source1)); + + program.redo(); + + assertFalse(sourceManager.containsSourceFile(source1)); + } + + @Test + public void testUndoAddingSourceFile() throws LockException, IOException { + SourceFile test = new SourceFile("/a/b/c.h"); + int txId = program.startTransaction("adding source file"); + try { + sourceManager.addSourceFile(test); + } + finally { + program.endTransaction(txId, true); + } + + assertTrue(sourceManager.containsSourceFile(test)); + + program.undo(); + + assertFalse(sourceManager.containsSourceFile(test)); + + program.redo(); + + assertTrue(sourceManager.containsSourceFile(test)); + } + + @Test + public void testGetUri() throws URISyntaxException { + String path = "/src/test.c"; + URI uri = new URI("file", null, path, null); + SourceFile testFile = new SourceFile(path); + assertEquals(uri, testFile.getUri()); + } + + @Test + public void testIdentifierDisplayString() { + String path = "/src/test/file.c"; + HexFormat hexFormat = HexFormat.of(); + + SourceFile sourceFile = new SourceFile(path, SourceFileIdType.MD5, + hexFormat.parseHex("0123456789abcdef0123456789abcdef")); + assertEquals(SourceFileIdType.MD5, sourceFile.getIdType()); + assertEquals(sourceFile.getIdAsString(), "0123456789abcdef0123456789abcdef"); + + sourceFile = new SourceFile(path); + assertEquals(SourceFileIdType.NONE, sourceFile.getIdType()); + assertEquals(StringUtils.EMPTY, sourceFile.getIdAsString()); + + sourceFile = new SourceFile(path, SourceFileIdType.SHA1, + hexFormat.parseHex("0123456789abcdef0123456789abcdef01234567")); + assertEquals(SourceFileIdType.SHA1, sourceFile.getIdType()); + assertEquals("0123456789abcdef0123456789abcdef01234567", sourceFile.getIdAsString()); + + sourceFile = new SourceFile(path, SourceFileIdType.SHA256, + hexFormat.parseHex("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")); + assertEquals(SourceFileIdType.SHA256, sourceFile.getIdType()); + assertEquals("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + sourceFile.getIdAsString()); + + sourceFile = new SourceFile(path, SourceFileIdType.SHA512, + hexFormat.parseHex( + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef")); + assertEquals( + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + sourceFile.getIdAsString()); + + sourceFile = new SourceFile(path, SourceFileIdType.TIMESTAMP_64, Longs.toByteArray(0)); + assertEquals(SourceFileIdType.TIMESTAMP_64, sourceFile.getIdType()); + assertEquals("1970-01-01T00:00:00Z", sourceFile.getIdAsString()); + + sourceFile = new SourceFile(path, SourceFileIdType.UNKNOWN, new byte[] { 0x12, 0x13 }); + assertEquals(SourceFileIdType.UNKNOWN, sourceFile.getIdType()); + assertEquals("1213", sourceFile.getIdAsString()); + } + + @Test + public void testSamePathDifferentIdentifiers() throws LockException { + HexFormat hexFormat = HexFormat.of(); + + assertEquals(3, sourceManager.getAllSourceFiles().size()); + String path = "/src/test/file.c"; + SourceFile test1 = new SourceFile(path); + SourceFile test2 = new SourceFile(path, SourceFileIdType.MD5, + hexFormat.parseHex("0123456789abcdef0123456789abcdef")); + SourceFile test3 = + new SourceFile(path, SourceFileIdType.TIMESTAMP_64, Longs.toByteArray(0)); + + assertNotEquals(test1, test2); + assertNotEquals(test1, test3); + assertNotEquals(test2, test3); + + assertFalse(sourceManager.containsSourceFile(test1)); + assertFalse(sourceManager.containsSourceFile(test2)); + assertFalse(sourceManager.containsSourceFile(test3)); + + int txId = program.startTransaction("adding source file"); + try { + sourceManager.addSourceFile(test1); + } + finally { + program.endTransaction(txId, true); + } + + List sourceFiles = sourceManager.getAllSourceFiles(); + assertEquals(4, sourceFiles.size()); + + assertTrue(sourceManager.containsSourceFile(test1)); + assertFalse(sourceManager.containsSourceFile(test2)); + assertFalse(sourceManager.containsSourceFile(test3)); + + txId = program.startTransaction("adding source file"); + try { + sourceManager.addSourceFile(test2); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getAllSourceFiles(); + assertEquals(5, sourceFiles.size()); + + assertTrue(sourceManager.containsSourceFile(test1)); + assertTrue(sourceManager.containsSourceFile(test2)); + assertFalse(sourceManager.containsSourceFile(test3)); + + txId = program.startTransaction("adding source file"); + try { + sourceManager.addSourceFile(test3); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getAllSourceFiles(); + assertEquals(6, sourceFiles.size()); + + assertTrue(sourceManager.containsSourceFile(test1)); + assertTrue(sourceManager.containsSourceFile(test2)); + assertTrue(sourceManager.containsSourceFile(test3)); + + //repeat adding test3 + txId = program.startTransaction("adding source file"); + try { + sourceManager.addSourceFile(test3); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getAllSourceFiles(); + assertEquals(6, sourceFiles.size()); + + assertTrue(sourceManager.containsSourceFile(test1)); + assertTrue(sourceManager.containsSourceFile(test2)); + assertTrue(sourceManager.containsSourceFile(test3)); + + txId = program.startTransaction("removing source file"); + try { + sourceManager.removeSourceFile(test2); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getAllSourceFiles(); + assertEquals(5, sourceFiles.size()); + + assertTrue(sourceManager.containsSourceFile(test1)); + assertFalse(sourceManager.containsSourceFile(test2)); + assertTrue(sourceManager.containsSourceFile(test3)); + + } + + @Test + public void testSameIdentifierDifferentPaths() throws LockException { + HexFormat hexFormat = HexFormat.of(); + assertEquals(3, sourceManager.getAllSourceFiles().size()); + byte[] md5 = hexFormat.parseHex("0123456789abcdef0123456789abcdef"); + + SourceFile test1 = new SourceFile("/src/file1.c", SourceFileIdType.MD5, md5); + SourceFile test2 = new SourceFile("/src/file2.c", SourceFileIdType.MD5, md5); + SourceFile test3 = new SourceFile("/src/file3.c", SourceFileIdType.MD5, md5); + + assertNotEquals(test1, test2); + assertNotEquals(test1, test3); + assertNotEquals(test2, test3); + + assertFalse(sourceManager.containsSourceFile(test1)); + assertFalse(sourceManager.containsSourceFile(test2)); + assertFalse(sourceManager.containsSourceFile(test3)); + + int txId = program.startTransaction("adding source file"); + try { + sourceManager.addSourceFile(test1); + } + finally { + program.endTransaction(txId, true); + } + + List sourceFiles = sourceManager.getAllSourceFiles(); + assertEquals(4, sourceFiles.size()); + + assertTrue(sourceManager.containsSourceFile(test1)); + assertFalse(sourceManager.containsSourceFile(test2)); + assertFalse(sourceManager.containsSourceFile(test3)); + + txId = program.startTransaction("adding source file"); + try { + sourceManager.addSourceFile(test2); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getAllSourceFiles(); + assertEquals(5, sourceFiles.size()); + + assertTrue(sourceManager.containsSourceFile(test1)); + assertTrue(sourceManager.containsSourceFile(test2)); + assertFalse(sourceManager.containsSourceFile(test3)); + + txId = program.startTransaction("adding source file"); + try { + sourceManager.addSourceFile(test3); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getAllSourceFiles(); + assertEquals(6, sourceFiles.size()); + + assertTrue(sourceManager.containsSourceFile(test1)); + assertTrue(sourceManager.containsSourceFile(test2)); + assertTrue(sourceManager.containsSourceFile(test3)); + + //repeat adding test3 + txId = program.startTransaction("adding source file"); + try { + sourceManager.addSourceFile(test3); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getAllSourceFiles(); + assertEquals(6, sourceFiles.size()); + + assertTrue(sourceManager.containsSourceFile(test1)); + assertTrue(sourceManager.containsSourceFile(test2)); + assertTrue(sourceManager.containsSourceFile(test3)); + + txId = program.startTransaction("removing source file"); + try { + sourceManager.removeSourceFile(test2); + } + finally { + program.endTransaction(txId, true); + } + + sourceFiles = sourceManager.getAllSourceFiles(); + assertEquals(5, sourceFiles.size()); + + assertTrue(sourceManager.containsSourceFile(test1)); + assertFalse(sourceManager.containsSourceFile(test2)); + assertTrue(sourceManager.containsSourceFile(test3)); + + } + + @Test + public void testNoIdentifierNonNullArray() { + SourceFile sourceFile = + new SourceFile("/src/file.c", SourceFileIdType.NONE, new byte[] { 0x11, 0x22 }); + assertEquals("/src/file.c", sourceFile.getPath()); + assertEquals(SourceFileIdType.NONE, sourceFile.getIdType()); + + // array passed to SourceFile constructor should be ignored + assertTrue(Arrays.equals(new byte[0], sourceFile.getIdentifier())); + } + + @Test(expected = IllegalArgumentException.class) + public void testBadMd5Length() { + new SourceFile("/file.c", SourceFileIdType.MD5, new byte[] { 0x11, 0x22 }); + } + + @Test(expected = IllegalArgumentException.class) + public void testMd5NullArray() { + new SourceFile("/file.c", SourceFileIdType.MD5, null); + } + + @Test(expected = NullPointerException.class) + public void testAddingNullSourceFile() throws LockException { + int txId = program.startTransaction("adding null SourceFile"); + try { + sourceManager.addSourceFile(null); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = NullPointerException.class) + public void testRemovingNullSourceFile() throws LockException { + int txId = program.startTransaction("removing null source file"); + try { + assertFalse(sourceManager.removeSourceFile(null)); + } + finally { + program.endTransaction(txId, true); + } + } + + @Test(expected = NullPointerException.class) + public void testConvertingNullArrayToLong() { + SourceFileUtils.byteArrayToLong(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testConvertingEmptyArrayToLong() { + SourceFileUtils.byteArrayToLong(new byte[0]); + } + + @Test + public void testLongConversion() { + long testLong = 0x0102030405060708L; + byte[] testArray = new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8 }; + assertEquals(testLong, SourceFileUtils.byteArrayToLong(testArray)); + assertTrue(Arrays.equals(testArray, SourceFileUtils.longToByteArray(testLong))); + } + + @Test + public void testHexStringToByteArrayConversion() { + assertTrue(Arrays.equals(new byte[0], SourceFileUtils.hexStringToByteArray(null))); + assertTrue( + Arrays.equals(new byte[0], SourceFileUtils.hexStringToByteArray(StringUtils.EMPTY))); + assertTrue(Arrays.equals(new byte[0], SourceFileUtils.hexStringToByteArray(" "))); + byte[] testArray = new byte[] { 0x00, 0x01, (byte) 0xaa, (byte) 0xff }; + assertTrue(Arrays.equals(testArray, SourceFileUtils.hexStringToByteArray("0001aaff"))); + assertTrue(Arrays.equals(testArray, SourceFileUtils.hexStringToByteArray("0001AAFF"))); + assertTrue(Arrays.equals(testArray, SourceFileUtils.hexStringToByteArray("0X0001AAFF"))); + assertTrue(Arrays.equals(testArray, SourceFileUtils.hexStringToByteArray("0x0001aaff"))); + } + + @Test + public void testByteArrayToHexStringConversion() { + assertEquals(StringUtils.EMPTY, SourceFileUtils.byteArrayToHexString(null)); + assertEquals(StringUtils.EMPTY, SourceFileUtils.byteArrayToHexString(new byte[0])); + assertEquals("00112233445566778899aabbccddeeff", + SourceFileUtils.byteArrayToHexString(new byte[] { 0x0, 0x11, 0x22, 0x33, 0x44, 0x55, + 0x66, + 0x77, (byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd, + (byte) 0xee, (byte) 0xff })); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/SourceMapEntryIteratorTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/SourceMapEntryIteratorTest.java new file mode 100644 index 0000000000..5dcdd61adb --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/SourceMapEntryIteratorTest.java @@ -0,0 +1,222 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database.sourcemap; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import ghidra.framework.store.LockException; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressOverflowException; +import ghidra.program.model.sourcemap.*; + +public class SourceMapEntryIteratorTest extends AbstractSourceFileTest { + + @Test + public void testInappropriateAddress() { + assertFalse(sourceManager.getSourceMapEntryIterator(null, true).hasNext()); + assertFalse(sourceManager.getSourceMapEntryIterator(null, false).hasNext()); + assertFalse(sourceManager.getSourceMapEntryIterator(Address.NO_ADDRESS, true).hasNext()); + assertFalse(sourceManager.getSourceMapEntryIterator(Address.NO_ADDRESS, false).hasNext()); + } + + @Test + public void testDummySourceManagerIterators() { + Address address = ret2_1.getAddress(); + assertFalse( + SourceFileManager.DUMMY.getSourceMapEntryIterator(address, true).hasNext()); + assertFalse( + SourceFileManager.DUMMY.getSourceMapEntryIterator(address, false).hasNext()); + } + + @Test(expected = NoSuchElementException.class) + public void testNoSuchElementExceptionForward() throws AddressOverflowException, LockException { + SourceMapEntry entry1 = null; + + int txId = program.startTransaction("adding source map entry"); + try { + entry1 = sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), 2); + } + finally { + program.endTransaction(txId, true); + } + + SourceMapEntryIterator iter = + sourceManager.getSourceMapEntryIterator(ret2_1.getAddress(), true); + assertTrue(iter.hasNext()); + assertEquals(entry1, iter.next()); + assertFalse(iter.hasNext()); + iter.next(); + } + + @Test(expected = NoSuchElementException.class) + public void testNoSuchElementExceptionBackward() + throws AddressOverflowException, LockException { + SourceMapEntry entry1 = null; + + int txId = program.startTransaction("adding source map entry"); + try { + entry1 = sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), 2); + } + finally { + program.endTransaction(txId, true); + } + + SourceMapEntryIterator iter = + sourceManager.getSourceMapEntryIterator(ret2_1.getAddress(), false); + assertTrue(iter.hasNext()); + assertEquals(entry1, iter.next()); + assertFalse(iter.hasNext()); + iter.next(); + } + + @Test + public void testForwardIterator() throws AddressOverflowException, LockException { + int txId = program.startTransaction("adding first source map entry"); + SourceMapEntry entry1 = null; + SourceMapEntry entry2 = null; + SourceMapEntry entry3 = null; + SourceMapEntry entry4 = null; + SourceMapEntry entry5 = null; + + SourceMapEntryIterator iter = + sourceManager.getSourceMapEntryIterator(ret2_1.getAddress(), true); + for (int i = 0; i < 10; i++) { + assertFalse(iter.hasNext()); + } + + try { + entry1 = sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), 2); + entry2 = sourceManager.addSourceMapEntry(source1, 2, ret2_1.getAddress(), 2); + entry3 = sourceManager.addSourceMapEntry(source2, 3, ret2_1.getAddress().add(1), 0); + entry4 = sourceManager.addSourceMapEntry(source3, 4, nop1_1.getAddress(), 1); + entry5 = sourceManager.addSourceMapEntry(source3, 5, nop1_1.getAddress(), 1); + } + finally { + program.endTransaction(txId, true); + } + Set entries = new HashSet<>(); + iter = sourceManager.getSourceMapEntryIterator(ret2_1.getAddress(), true); + + for (int i = 0; i < 10; i++) { + assertTrue(iter.hasNext()); + } + entries.add(iter.next()); + + assertTrue(iter.hasNext()); + entries.add(iter.next()); + assertTrue(entries.contains(entry1)); + assertTrue(entries.contains(entry2)); + + entries.clear(); + + assertTrue(iter.hasNext()); + assertEquals(entry3, iter.next()); + + assertTrue(iter.hasNext()); + entries.add(iter.next()); + assertTrue(iter.hasNext()); + entries.add(iter.next()); + assertTrue(entries.contains(entry4)); + assertTrue(entries.contains(entry5)); + assertFalse(iter.hasNext()); + entries.clear(); + + iter = sourceManager.getSourceMapEntryIterator(ret2_1.getAddress().add(1), true); + assertTrue(iter.hasNext()); + assertEquals(entry3, iter.next()); + + assertTrue(iter.hasNext()); + entries.add(iter.next()); + assertTrue(iter.hasNext()); + entries.add(iter.next()); + assertTrue(entries.contains(entry4)); + assertTrue(entries.contains(entry5)); + assertFalse(iter.hasNext()); + + iter = sourceManager.getSourceMapEntryIterator(ret2_2.getAddress(), true); + assertFalse(iter.hasNext()); + } + + @Test + public void testBackwardIterator() throws AddressOverflowException, LockException { + int txId = program.startTransaction("adding first source map entry"); + SourceMapEntry entry1 = null; + SourceMapEntry entry2 = null; + SourceMapEntry entry3 = null; + SourceMapEntry entry4 = null; + SourceMapEntry entry5 = null; + + SourceMapEntryIterator iter = + sourceManager.getSourceMapEntryIterator(ret2_1.getAddress(), false); + for (int i = 0; i < 10; i++) { + assertFalse(iter.hasNext()); + } + + try { + entry1 = sourceManager.addSourceMapEntry(source1, 1, ret2_1.getAddress(), 2); + entry2 = sourceManager.addSourceMapEntry(source1, 2, ret2_1.getAddress(), 2); + entry3 = sourceManager.addSourceMapEntry(source2, 3, ret2_1.getAddress().add(1), 0); + entry4 = sourceManager.addSourceMapEntry(source3, 4, nop1_1.getAddress(), 1); + entry5 = sourceManager.addSourceMapEntry(source3, 5, nop1_1.getAddress(), 1); + } + finally { + program.endTransaction(txId, true); + } + Set entries = new HashSet<>(); + iter = sourceManager.getSourceMapEntryIterator(nop1_1.getAddress(), false); + + for (int i = 0; i < 10; i++) { + assertTrue(iter.hasNext()); + } + entries.add(iter.next()); + assertTrue(iter.hasNext()); + entries.add(iter.next()); + assertTrue(entries.contains(entry4)); + assertTrue(entries.contains(entry5)); + + entries.clear(); + + assertTrue(iter.hasNext()); + assertEquals(entry3, iter.next()); + + assertTrue(iter.hasNext()); + entries.add(iter.next()); + assertTrue(iter.hasNext()); + entries.add(iter.next()); + assertTrue(entries.contains(entry1)); + assertTrue(entries.contains(entry1)); + assertFalse(iter.hasNext()); + entries.clear(); + + iter = sourceManager.getSourceMapEntryIterator(ret2_1.getAddress().add(1), false); + assertTrue(iter.hasNext()); + assertEquals(entry3, iter.next()); + + assertTrue(iter.hasNext()); + entries.add(iter.next()); + assertTrue(iter.hasNext()); + entries.add(iter.next()); + assertTrue(entries.contains(entry1)); + assertTrue(entries.contains(entry2)); + assertFalse(iter.hasNext()); + + } + +}