Merge branch 'GP-3947_ghidravision_DecompilerDebugXmlLoader_final'

This commit is contained in:
Ryan Kurtz
2025-09-12 08:21:29 -04:00
10 changed files with 3246 additions and 4 deletions
@@ -1086,4 +1086,5 @@ src/test.slow/resources/filterTestDirList.txt||GHIDRA||||END|
src/test.slow/resources/ghidra/app/plugin/core/datamgr/TestDataType.txt||GHIDRA||||END|
src/test.slow/resources/ghidra/app/script/GhidraScriptAsk.properties||GHIDRA||||END|
src/test/resources/defaultTools/TestCodeBrowser.tool||GHIDRA||||END|
src/test/resources/ghidra/app/util/opinion/decompile_debug_test.xml||GHIDRA||||END|
src/test/resources/ghidra/app/util/opinion/test.ord||GHIDRA||||END|
@@ -0,0 +1,114 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
*
*/
package ghidra.app.util.opinion;
import static ghidra.program.model.pcode.AttributeId.*;
import java.util.HexFormat;
import ghidra.framework.store.LockException;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressOverflowException;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import ghidra.xml.*;
/**
* Manager to hold byte information from the <bytechunk> tags inside the Decompiler Debug's XML.
*/
public class DecompileDebugByteManager {
TaskMonitor monitor;
Program prog;
String programName;
/**
* @param monitor TaskMonitor
* @param prog main program
* @param programName name of program
*/
public DecompileDebugByteManager(TaskMonitor monitor,
Program prog, String programName) {
this.monitor = monitor;
this.prog = prog;
this.programName = programName;
}
/**
* Parse the <bytechunk> tag - has the memory offset and the raw bytes
*
* @param parser XmlPullParser
* @param log Xml
*/
public void parse(XmlPullParser parser, XmlMessageLog log) {
while (parser.peek().getName().equals("bytechunk") && !monitor.isCancelled()) {
processByteChunk(parser, log);
}
}
/**
* Handle parsing and creating bytechunks & pulling out the byte string as a byte array.
*
* @param parser XmlPullParser
* @param log XmlMessageLog
*/
private void processByteChunk(XmlPullParser parser, XmlMessageLog log) {
XmlElement byteChunkElement = parser.start("bytechunk");
Address address =
prog.getAddressFactory()
.getAddress(byteChunkElement.getAttribute(ATTRIB_OFFSET.name()));
// end element contains the byte content
byteChunkElement = parser.end(byteChunkElement);
String hexString = byteChunkElement.getText().trim().replaceAll("\n", "");
byte[] rawBytes = HexFormat.of().parseHex(hexString);
if (!generateMemoryChunk(rawBytes, address, log)) {
log.appendMsg("Error attempting to load memory chunk");
}
}
/**
* Create memory blocks with the raw bytes from the central function and any other blocks
* such as for pointers or other data types that the Decompile.xml file was generated from.
*
* @param rawBytes raw program bytes
* @param address memory offset
* @param log XmlMessageLog
*/
private boolean generateMemoryChunk(byte[] rawBytes, Address address, XmlMessageLog log) {
Memory memory = prog.getMemory();
try {
memory.createInitializedBlock(programName, address, rawBytes.length, (byte) 0,
monitor, false);
memory.setBytes(address, rawBytes);
}
catch (LockException | IllegalArgumentException | MemoryConflictException
| AddressOverflowException | CancelledException | MemoryAccessException e) {
log.appendException(e);
return false;
}
return true;
}
}
@@ -0,0 +1,470 @@
/* ###
* 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.opinion;
import static ghidra.program.model.pcode.AttributeId.*;
import java.math.BigInteger;
import java.util.Map;
import java.util.TreeMap;
import ghidra.program.model.data.*;
import ghidra.program.model.data.Enum;
import ghidra.program.model.listing.Program;
import ghidra.program.model.pcode.AttributeId;
import ghidra.util.task.TaskMonitor;
import ghidra.util.xml.SpecXmlUtils;
import ghidra.xml.*;
/**
* Manager for parsing and storing data type objects from the XML - identified by
* the <coretypes> and <typegrp> tags.
*
* NOTE: In the typegrp subtree, ID is often on a different line from the element's name and
* metatype, so we need a way to reference this for use in the map -- String idHolder var
* helps with this.
*/
public class DecompileDebugDataTypeManager {
TaskMonitor monitor;
Program prog;
Map<DataTypeKey, DataType> dataTypeMap;
DataTypeManager programDataManager;
BuiltInDataTypeManager builtInMngr = BuiltInDataTypeManager.getDataTypeManager();
private String idHolder;
/**
* @param monitor TaskMonitor
* @param prog main program info
*/
public DecompileDebugDataTypeManager(TaskMonitor monitor, Program prog) {
this.monitor = monitor;
this.prog = prog;
this.dataTypeMap = new TreeMap<DataTypeKey, DataType>();
programDataManager = this.prog.getListing().getDataTypeManager();
}
/**
* Parse Data Type tag, handling types:
* <type>
* <typeref>
* <def>
* <void>
* @param parser XmlPullParser
* @param log XmlMessageLog
*
* @return retrieved DataType
*/
public DataType parseDataTypeTag(XmlPullParser parser, XmlMessageLog log) {
String tagName = parser.peek().getName();
DataType retrieved = null;
switch (tagName) {
case "type":
retrieved = parseType(parser, log);
break;
case "typeref":
retrieved = parseRefType(parser, log);
break;
case "def":
retrieved = parseDef(parser, log);
break;
case "void":
XmlElement voidElement = parser.start("void");
parser.end(voidElement);
return new VoidDataType();
default:
log.appendMsg(parser.getLineNumber(), "Level " + parser.getCurrentLevel() +
" tag not currently supported: " + tagName);
parser.discardSubTree();
}
return retrieved;
}
/**
* Parse the <type> tag
*
* @param parser XmlPullParser
* @param log XmlMessageLog
*/
private DataType parseType(XmlPullParser parser, XmlMessageLog log) {
DataType retrieved = null;
String metatype = parser.peek().getAttribute("metatype");
if (metatype == null) { // in the typegrp subtree, metatype and name/id are not on the same line
retrieved = retreiveBaseType(parser, log);
return retrieved;
}
switch (metatype) {
case "ptr":
retrieved = parsePointer(parser, log);
break;
case "ptrrel":
retrieved = parsePointerRelative(parser, log);
break;
case "array":
retrieved = parseArray(parser, log);
break;
case "struct":
retrieved = parseStruct(parser, log);
break;
case "union":
retrieved = parseUnion(parser, log);
break;
case "enum_int":
case "enum_uint":
retrieved = parseEnum(parser, log);
break;
default:
retrieved = retreiveBaseType(parser, log);
}
return retrieved;
}
/**
* TypeDefs (<def> tags) are new definitions of types - basically, a re-naming.
*
* @param parser XmlPullParser
* @param log XmlMessageLog
*
* @return retrieved DataType
*/
private DataType parseDef(XmlPullParser parser, XmlMessageLog log) {
XmlElement defElement = parser.start("def");
DataTypeKey key = new DataTypeKey(defElement);
if (!dataTypeMap.containsKey(key)) {
DataType typeDefedType = parseDataTypeTag(parser, log);
TypedefDataType generatedTypeDef = new TypedefDataType(
new CategoryPath(CategoryPath.ROOT + key.name()), key.name(), typeDefedType,
programDataManager);
DataType resolvedDT = resolveAndMapDataType(key, generatedTypeDef);
parser.end(defElement);
return resolvedDT;
}
return dataTypeMap.get(key);
}
/**
* Parse and handle enum types - signed/unsigned
* @param parser XmlPullParser
* @param log XmlMessageLog
*
* @return resolved DataType
*/
private DataType parseEnum(XmlPullParser parser, XmlMessageLog log) {
XmlElement enumElement = parser.start("type");
DataTypeKey key = new DataTypeKey(enumElement);
int length = SpecXmlUtils.decodeInt(enumElement.getAttribute(ATTRIB_SIZE.name()));
Enum enumDT = null;
if (dataTypeMap.containsKey(key) == false) {
enumDT = new EnumDataType(new CategoryPath(CategoryPath.ROOT + key.name()), key.name(),
length,
programDataManager);
enumDT =
(Enum) resolveAndMapDataType(key, enumDT);
}
else {
enumDT = (Enum) dataTypeMap.get(key);
}
while (parser.peek().getName().equals(ATTRIB_VAL.name())) {
XmlElement valElement = parser.start(ATTRIB_VAL.name());
enumDT.add(valElement.getAttribute(ATTRIB_NAME.name()),
SpecXmlUtils.decodeInt(valElement.getAttribute(ATTRIB_VALUE.name())), "");
parser.end(valElement);
}
parser.end(enumElement);
return enumDT;
}
/**
* Parse & create union types
* @param parser XmlPullParser
* @param log XmlMessageLog
*
* @return resolved DataType
*/
private DataType parseUnion(XmlPullParser parser, XmlMessageLog log) {
XmlElement unionElement = parser.start("type");
DataTypeKey key = new DataTypeKey(unionElement);
int size = SpecXmlUtils.decodeInt(unionElement.getAttribute(ATTRIB_SIZE.name()));
Union unionDT = null;
if (dataTypeMap.containsKey(key) == false) {
unionDT =
new UnionDataType(new CategoryPath(CategoryPath.ROOT + key.name()), key.name(),
programDataManager);
unionDT = (Union) resolveAndMapDataType(key, unionDT);
}
else {
unionDT = (Union) dataTypeMap.get(key);
}
if (unionElement.hasAttribute(ATTRIB_INCOMPLETE.name()) || size == 0) {
parser.end(unionElement);
return unionDT;
}
while (parser.peek().getName().equals("field")) {
XmlElement fieldElement = parser.start("field");
DataType fieldDT = parseDataTypeTag(parser, log);
unionDT.add(fieldDT, fieldDT.getLength(), key.name(), "");
parser.end(fieldElement);
}
parser.end(unionElement);
return unionDT;
}
/**
* Parse and process Array Data Type
* @param parser XmlPullParser
* @param log XmlMessageLog
*
* @return DataType
*/
private DataType parseArray(XmlPullParser parser, XmlMessageLog log) {
XmlElement arrayElement = parser.start("type");
int arraySize =
SpecXmlUtils.decodeInt(arrayElement.getAttribute(AttributeId.ATTRIB_ARRAYSIZE.name()));
DataType baseType = parseDataTypeTag(parser, log);
ArrayDataType arrayDT =
new ArrayDataType(baseType, arraySize, baseType.getLength(), programDataManager);
DataType resolved = resolveAndMapDataType(
new DataTypeKey(baseType.getName() + "array", idHolder), arrayDT);
parser.end(arrayElement);
return resolved;
}
/**
* Handle parsing and creating a pointer data type. Add to the programDataManager and the
* DataTypeMap for use later.
* @param parser XmlPullParser
* @param log XmlMessageLog
* @return generated pointer data type
*/
private DataType parsePointer(XmlPullParser parser, XmlMessageLog log) {
XmlElement pointerElement = parser.start("type");
int size = SpecXmlUtils.decodeInt(pointerElement.getAttribute(ATTRIB_SIZE.name()));
DataType baseType = parseDataTypeTag(parser, log);
PointerDataType pointerDT = new PointerDataType(baseType, size, programDataManager);
DataType resolved =
resolveAndMapDataType(new DataTypeKey(baseType.getName() + "ptr", idHolder),
pointerDT);
parser.end(pointerElement);
return resolved;
}
/**
* Parse and handle 'pointer with offset' data types: PointerTypeDef {@PointerTypedef.java line #91}.
* These are useful for labeling a variable that points into the interior of a structure,
* but where the compiler still knows it can access the whole structure.
*
* @param parser XmlPullParser
* @param log XmlMessageLog
* @return DataType - PointerTypeDef
*/
private DataType parsePointerRelative(XmlPullParser parser, XmlMessageLog log) {
XmlElement pointerRelElement = parser.start("type");
int size = SpecXmlUtils.decodeInt(pointerRelElement.getAttribute(ATTRIB_SIZE.name()));
Long offset = SpecXmlUtils.decodeLong(pointerRelElement.getAttribute(ATTRIB_OFF.name()));
DataType baseType = parseDataTypeTag(parser, log);
PointerTypedef relPointerDT =
new PointerTypedef(baseType.getName(), baseType, size, programDataManager, offset);
DataType resolved = resolveAndMapDataType(new DataTypeKey(baseType.getName()+"relptr", idHolder), relPointerDT);
parser.end(pointerRelElement);
return resolved;
}
/**
* Handle parsing and generating a struct type; populating it with fields.
*
* @param parser XmlPullParser
* @param log XmlMessageLog
* @return Structure data type - either an empty one or completed one with fields
*/
private DataType parseStruct(XmlPullParser parser, XmlMessageLog log) {
XmlElement structElement = parser.start("type");
DataTypeKey key = new DataTypeKey(structElement);
int size = SpecXmlUtils.decodeInt(structElement.getAttribute(ATTRIB_SIZE.name()));
Structure createdStruct = null;
if (dataTypeMap.containsKey(key) == false) {
StructureDataType newStruct =
new StructureDataType(key.name(), size, programDataManager);
createdStruct = (Structure) resolveAndMapDataType(key, newStruct);
}
else {
createdStruct =
(Structure) dataTypeMap.get(key);
}
if (structElement.hasAttribute(ATTRIB_INCOMPLETE.name()) || size == 0) {
parser.end(structElement);
return createdStruct;
}
while (parser.peek().getName().equals("field")) {
XmlElement fieldElement = parser.start("field");
int fieldOffset =
SpecXmlUtils.decodeInt(fieldElement.getAttribute(ATTRIB_OFFSET.name()));
DataType fieldDT = parseDataTypeTag(parser, log);
createdStruct.replaceAtOffset(fieldOffset, fieldDT, fieldDT.getLength(),
fieldElement.getAttribute(ATTRIB_NAME.name()), "");
parser.end(fieldElement);
}
parser.end(structElement);
return createdStruct;
}
/**
* The type should already exist in the map and in the Program's data type manager.
* If it's not there, this is an error.
*
* @param parser XmlPullParser
* @param log XmlMessageLog
*
* @return retrieved DataType or null if it's not actually in the map (this should never happen)
*/
private DataType parseRefType(XmlPullParser parser, XmlMessageLog log) {
XmlElement typeRefElement = parser.start("typeref");
DataTypeKey key = new DataTypeKey(typeRefElement);
DataType dt = dataTypeMap.get(key);
if (dt == null) {
log.appendMsg("Data Type referenced without first being created.");
}
parser.end(typeRefElement);
return dt;
}
/**
* Pull the core type from the data type map
* @param parser XmlPullParser
* @param log XmlMessageLog
*
* @return Retrieved DataType
*/
private DataType retreiveBaseType(XmlPullParser parser, XmlMessageLog log) {
XmlElement typeElement = parser.start("type");
DataType retrieved = null;
DataTypeKey key = new DataTypeKey(typeElement);
// We need to be able to reference the current ID when building composite DTs
idHolder = key.id();
if (dataTypeMap.containsKey(key)) {
retrieved = dataTypeMap.get(key);
}
else {
retrieved = builtInMngr.getDataType(CategoryPath.ROOT, key.name().toLowerCase()); // there are some cases where the DT name is defined in lowercase and then later referred inconsistently
if (retrieved != null) {
retrieved = resolveAndMapDataType(key, retrieved);
}
else {
log.appendMsg("Type tag " + key.name() + " didn't resolve");
}
}
parser.end(typeElement);
return retrieved;
}
/**
* Resolve the provided DataType against the Program Manager then add it to the DataTypeMap
* for referencing later.
* @param key DataTypeKey consisting of a name and ID for referencing
* @param generatedTypeDef DataType identified Data Type for resolving and mapping
*
* @return resolved DataType
*/
private DataType resolveAndMapDataType(DataTypeKey key, DataType generatedTypeDef) {
DataType resolvedDT = programDataManager.resolve(generatedTypeDef, dtConflictHandler);
dataTypeMap.put(key, resolvedDT);
return resolvedDT;
}
public static DataTypeConflictHandler dtConflictHandler = new DataTypeConflictHandler() {
@Override
public ConflictResult resolveConflict(DataType addedDataType, DataType existingDataType) {
return ConflictResult.RENAME_AND_ADD;
}
@Override
public boolean shouldUpdate(DataType sourceDataType, DataType localDataType) {
return true;
}
@Override
public DataTypeConflictHandler getSubsequentHandler() {
return DEFAULT_HANDLER;
}
};
/**
* Data Types are organized by their name and ID; utilize a combo key structure for this organization
* Name, ID -> DataType
* @param name String data type name
* @param id String data type id
* @param val BigInteger to account for ID's that aren't written in Hex.
*/
static record DataTypeKey(String name, String id, BigInteger val)
implements Comparable<DataTypeKey> {
public DataTypeKey(XmlElement typeRefElement) {
this(typeRefElement.getAttribute(ATTRIB_NAME.name()),
typeRefElement.getAttribute(ATTRIB_ID.name()));
}
public DataTypeKey(String name, String id) {
this(name, id,
(id.startsWith("0x") ? new BigInteger(id.substring(2), 16)
: new BigInteger(id, 10)));
}
public DataTypeKey(String name, String id, BigInteger val) {
this.name = name;
this.id = id;
this.val = val;
}
@Override
public int compareTo(DataTypeKey other) {
int nameCompare = this.name.compareTo(other.name);
if (nameCompare != 0) {
return nameCompare;
}
return (int) (this.val.longValue() - other.val.longValue());
}
}
}
@@ -0,0 +1,347 @@
/* ###
* 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.opinion;
import static ghidra.program.model.pcode.AttributeId.*;
import java.util.ArrayList;
import java.util.Map;
import ghidra.framework.store.LockException;
import ghidra.program.database.function.OverlappingFunctionException;
import ghidra.program.model.address.*;
import ghidra.program.model.data.DataType;
import ghidra.program.model.listing.*;
import ghidra.program.model.listing.Function.FunctionUpdateType;
import ghidra.program.model.mem.Memory;
import ghidra.program.model.mem.MemoryConflictException;
import ghidra.program.model.pcode.AddressXML;
import ghidra.program.model.symbol.Namespace;
import ghidra.program.model.symbol.SourceType;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
import ghidra.util.xml.SpecXmlUtils;
import ghidra.xml.*;
/**
* Manage the parsing and population of function objects. All functions are loaded the same way,
* even if it's only referenced by the "central function" (the function that was in the decompiler
* pane when the Decompile Debug (@DecompileDebug.java) feature was used).
*/
public class DecompileDebugFunctionManager {
Program prog;
TaskMonitor monitor;
DecompileDebugDataTypeManager dataTypeManager;
/**
* Each function requires a program, task monitor and the program's Data Type Manager in order
* to be generated.
*
* @param prog Program
* @param monitor TaskMonitor
* @param dataTypeManager Program's DataTypeManager
*/
public DecompileDebugFunctionManager(Program prog, TaskMonitor monitor,
DecompileDebugDataTypeManager dataTypeManager) {
this.prog = prog;
this.monitor = monitor;
this.dataTypeManager = dataTypeManager;
}
/**
* Setup functions from within the <symbollist> tag.
* Functions referenced by the central function are loaded the same except we do not
* (currently) have the memory/program context for them. Thus, they will show up in the Listing
* with a red "X".
* NOTE: This is the expected functionality.
*
* @param parser XmlPullParser
* @param scopeMap Map<String, Namespace> used for getting parent namespace
* @param log XmlMessageLog
*/
public void parseFunctionSignature(XmlPullParser parser, Map<Long, Namespace> scopeMap,
XmlMessageLog log) {
XmlElement functionElement = parser.start("function");
String functionName = functionElement.getAttribute(ATTRIB_NAME.name());
boolean noReturn = functionElement.hasAttribute(ATTRIB_NORETURN.name());
Function createdFunction = null;
XmlElement localdb = null;
Address functionAddr = null;
while (parser.peek().isStart()) {
String tagName = parser.peek().getName();
switch (tagName) {
case "addr":
try {
XmlElement addressTag = parser.start("addr");
functionAddr =
AddressXML.restoreXml(addressTag, prog.getCompilerSpec())
.getFirstAddress();
parser.end(addressTag);
break;
}
catch (XmlParseException e) {
log.appendException(e);
}
case "scope":
XmlElement scopeElement = parser.start("scope");
String scopeName = scopeElement.getAttribute(ATTRIB_NAME.name());
Namespace functionNamespace =
getParentNamespace(parser, scopeMap, scopeName);
createdFunction =
setFunctionNamespaceAndStorage(functionName, functionAddr,
functionNamespace,
log);
createdFunction.setNoReturn(noReturn);
handleScopeSubtags(createdFunction, parser, log);
parser.end(scopeElement);
parser.end(localdb);
break;
case "prototype":
XmlElement prototypeElement = parser.start("prototype");
try {
createdFunction
.setCallingConvention(prototypeElement.getAttribute("model"));
createdFunction.setReturnType(retrieveReturnType(parser, log),
SourceType.IMPORTED);
if (parser.peek().isStart("inject")) {
parser.start("inject");
String callFixup = parser.end().getText();
createdFunction.setCallFixup(callFixup);
log.appendMsg("Found an inject tag on the function: " +
createdFunction.getName());
}
}
catch (InvalidInputException e) {
log.appendException(e);
}
while (!parser.peek().isEnd()) {
log.appendMsg(parser.getLineNumber(), "Level " + parser.getCurrentLevel() +
" tag not currently supported: " + parser.peek().getName());
parser.discardSubTree();
}
parser.end(prototypeElement);
break;
case "localdb":
localdb = parser.start("localdb"); // this is a wrapper, it ends after the scope tag
break;
default:
log.appendMsg(parser.getLineNumber(), "Level " + parser.getCurrentLevel() +
" tag not currently supported: " + parser.peek().getName());
parser.discardSubTree();
}
}
parser.end(functionElement);
}
/**
* Parse the <parent> subtag under <scope> and generate the function's Namespace.
*
* @param parser XmlPullParser
* @param scopeMap Map<String, Namespace> used for looking up parent Namespace based on ID from <parent> tag
* @param scopeName String from <scope> tag
*
* @return Namespace generated namespace
*/
private Namespace getParentNamespace(XmlPullParser parser, Map<Long, Namespace> scopeMap,
String scopeName) {
XmlElement parentTag = parser.start("parent");
Long parentId = SpecXmlUtils.decodeLong(parentTag.getAttribute(ATTRIB_ID.name()));
Namespace parentNamespace = scopeMap.get(parentId);
parser.end(parentTag);
return parentNamespace;
}
/**
* Step through the <scope> subtags, including:
* - <parent>
* - <rangelist>
* - <symbollist>
*
* NOTE: A populated <rangelist> is not currently supported.
*
* @param createdFunction Function
* @param parser XmlPullParser
* @param log XmlMessageLog
*/
private void handleScopeSubtags(Function createdFunction,
XmlPullParser parser, XmlMessageLog log) {
while (parser.peek().isStart()) {
String tagName = parser.peek().getName();
switch (tagName) {
case "rangelist":
log.appendMsg(parser.getLineNumber(), "Level " + parser.getCurrentLevel() +
" tag not currently supported: " + tagName);
parser.discardSubTree(); // we currently do not support a populated rangelist
break;
case "symbollist":
XmlElement symbollistElement = parser.start("symbollist");
findFunctionVariables(parser, createdFunction, log);
parser.end(symbollistElement);
break;
default:
log.appendMsg(parser.getLineNumber(), "Level " + parser.getCurrentLevel() +
" tag not currently supported: " + tagName);
parser.discardSubTree();
}
}
}
/**
* Parse and retrieve the return type from the program's data type manager.
*
* @param parser XmlPullParser
* @param log XmlMessageLog
*
* @return DataType return type
*/
private DataType retrieveReturnType(XmlPullParser parser, XmlMessageLog log) {
XmlElement returnElement = parser.start("returnsym");
XmlElement addrElement = parser.start("addr");
//Address address = AddressXML.restoreXml(addrElement, prog.getCompilerSpec()).getFirstAddress();
parser.end(addrElement);
DataType returnType = dataTypeManager.parseDataTypeTag(parser, log);
parser.end(returnElement);
return returnType;
}
/**
* Parse and load parameter and local variables for the given function by
* cycling through the <mapsym> tags within the symbollist
*
* NOTE: populated <rangelist> tags within a mapsym tag are not currently supported.
*
* NOTE: Keep a list of parameters and modify the function only once
* (ie. 1 call to updateFunction()) after all variables have been parsed.
*
* @param parser XmlPullParser
* @param createdFunction Function
* @param log XmlMessageLog
*/
private void findFunctionVariables(XmlPullParser parser, Function createdFunction,
XmlMessageLog log) {
ArrayList<ParameterImpl> paramList = new ArrayList<ParameterImpl>();
try {
while (parser.peek().isStart()) {
XmlElement mapsymElement = parser.start("mapsym");
XmlElement symbolElement = parser.start("symbol");
boolean isParam = SpecXmlUtils.decodeInt(symbolElement.getAttribute("cat")) == 0;
String varName = symbolElement.getAttribute(ATTRIB_NAME.name());
DataType varType = dataTypeManager.parseDataTypeTag(parser, log);
parser.end(symbolElement);
XmlElement addrElement = parser.start("addr");
if (isParam) {
Address address = AddressXML.restoreXml(addrElement, prog.getCompilerSpec())
.getFirstAddress();
ParameterImpl param = new ParameterImpl(varName, varType,
address, prog);
paramList.add(param);
}
else {
int offset = (int) AddressXML.restoreXml(addrElement, prog.getCompilerSpec())
.getOffset();
LocalVariableImpl lvar =
new LocalVariableImpl(varName, varType, offset, prog, SourceType.IMPORTED);
createdFunction.addLocalVariable(lvar, SourceType.IMPORTED);
}
parser.end(addrElement);
while (parser.peek().isStart()) {
log.appendMsg(parser.getLineNumber(), "Level " + parser.getCurrentLevel() +
" tag not currently supported: " + parser.peek().getName());
parser.discardSubTree();
}
parser.end(mapsymElement);
createdFunction.updateFunction(null, null, paramList,
FunctionUpdateType.CUSTOM_STORAGE,
true, SourceType.IMPORTED); // calling convention & return variable are updated later
}
}
catch (NumberFormatException | InvalidInputException
| DuplicateNameException | XmlParseException e) {
log.appendException(e);
}
}
/**
* Set the Name and Namespace / custom variable storage on the function symbol after the
* initial function signature has been created.
*
* @param functionName name
* @param functionAddr address offset
* @param namespace Namespace parent namespace
* @param log XmlMessageLog
*
* @return generated Function
*/
private Function setFunctionNamespaceAndStorage(String functionName, Address functionAddr,
Namespace namespace, XmlMessageLog log) {
Function createdFunction = createFunction(functionName, functionAddr, namespace, log);
try {
createdFunction.setCustomVariableStorage(true);
Memory memory = prog.getMemory();
if (!memory.contains(functionAddr)) { // main function block has already been generated
memory.createInitializedBlock(functionName, functionAddr, 1, (byte) 0, monitor,
false);
}
}
catch (LockException | IllegalArgumentException
| MemoryConflictException | AddressOverflowException | CancelledException e) {
log.appendException(e);
}
return createdFunction;
}
/**
* Create Function object for populating later using the program's function manager.
* The central function is the one processed first.
*
* @param functionName String
* @param functionAddr Address
* @param namespace Namespace parent namespace
* @param log XmlMessageLog
* @return generated function
*/
private Function createFunction(String functionName, Address functionAddr,
Namespace namespace, XmlMessageLog log) {
FunctionManager funcM = prog.getFunctionManager();
Function generatedFunction = null;
try {
generatedFunction = funcM.createFunction(functionName, namespace, functionAddr,
new AddressSet(functionAddr, functionAddr), SourceType.IMPORTED);
}
catch (InvalidInputException | OverlappingFunctionException e) {
log.appendException(e);
}
return generatedFunction;
}
}
@@ -0,0 +1,324 @@
/* ###
* 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.opinion;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import ghidra.app.plugin.core.analysis.AnalysisWorker;
import ghidra.app.plugin.core.analysis.AutoAnalysisManager;
import ghidra.app.util.Option;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.importer.MessageLog;
import ghidra.framework.model.Project;
import ghidra.program.model.address.Address;
import ghidra.program.model.lang.*;
import ghidra.program.model.listing.Program;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Loader for handling XML files generated by the Decompiler on the central function that was in the
* Decompiler pane when the Debug Function Decompilation feature in the Decompiler is used.
*
* Reference @DecompileDebug.java for the generation of the XML file.
*/
public class DecompileDebugXmlLoader extends AbstractProgramLoader {
public static final String DECOMPILER_DEBUG_SRC_NAME = "Decompiler Debug XML";
@Override
public String getName() {
return DECOMPILER_DEBUG_SRC_NAME;
}
@Override
public LoaderTier getTier() {
return LoaderTier.SPECIALIZED_TARGET_LOADER;
}
@Override
public int getTierPriority() {
return 50;
}
/*
* This is an override method that identifies the load specs for the program to populate
* the initial upload dialog.
*
* @param provider
* @return the list of possible load specs (though really only one should work)
*/
@Override
public Collection<LoadSpec> findSupportedLoadSpecs(ByteProvider provider) throws IOException {
List<LoadSpec> loadSpecs = new ArrayList<>();
// the language service sets up the log, language map, and service
getLanguageService();
ParseGhidraDebugResult result = parse(provider);
DecompileDebugProgramInfo info = result.lastInfo;
if (info == null) {
// indicates that a different loader should be used
return loadSpecs;
}
LanguageID languageID = new LanguageID(info.specString());
LanguageDescription languageDescription =
getLanguageService().getLanguageDescription(languageID);
for (CompilerSpecDescription csd : languageDescription
.getCompatibleCompilerSpecDescriptions()) {
LanguageCompilerSpecPair pair =
new LanguageCompilerSpecPair(languageDescription.getLanguageID(),
csd.getCompilerSpecID());
if (info.compilerString().equals(csd.getCompilerSpecID().getIdAsString())) {
loadSpecs.add(new LoadSpec(this, 0, pair, true));
}
}
return loadSpecs;
}
/**
* After initial parsing of the XML file, load the details into a new program for
* loading/viewing in Ghidra.
*
* @param provider ByteProvider
* @param programName Program name from the XML
* @param project active project
* @param programFolderPath folder path to XML
* @param loadSpec String parsed out of the XML for details regarding language/compiler spec
* @param options program options - will always be empty
* @param log MessageLog
* @param consumer Object
* @param monitor TaskMonitor
*
* @return List of loaded programs - should always be 1
*/
@Override
protected List<Loaded<Program>> loadProgram(ByteProvider provider, String programName,
Project project,
String programFolderPath, LoadSpec loadSpec, List<Option> options, MessageLog log,
Object consumer,
TaskMonitor monitor) throws IOException, CancelledException {
LanguageCompilerSpecPair pair = loadSpec.getLanguageCompilerSpec();
Language importerLanguage = getLanguageService().getLanguage(pair.languageID);
CompilerSpec importerCompilerSpec =
importerLanguage.getCompilerSpecByID(pair.compilerSpecID);
ParseGhidraDebugResult parsedResult = parse(provider);
if (parsedResult.lastInfo == null) {
return new ArrayList<Loaded<Program>>();
}
Address imageBase = null;
if (parsedResult.lastInfo.offset() != null) {
imageBase =
importerLanguage.getAddressFactory().getAddress(parsedResult.lastInfo.offset());
}
Program prog = createProgram(provider, programName, imageBase, getName(),
importerLanguage, importerCompilerSpec, consumer);
List<Loaded<Program>> loadedList =
List.of(new Loaded<>(prog, programName, project, programFolderPath, consumer));
int loadingId = prog.startTransaction("Loading debug XML file");
try {
doImport(parsedResult.debugXmlMgr, options, log, prog, monitor, false, programName);
}
catch (
IllegalArgumentException e) {
throw new LoadException("Failed to load");
}
finally {
prog.endTransaction(loadingId, true);
}
return loadedList;
}
/**
* Import the XML; first parse and load details into the generation of a new program based
* around the "central" function that the Decompile Debug XML file was generated from.
*
* @param debugXmlMgr Manages the parsing of the XML objects
* @param options if there are any settings for importing work - these will always be empty
* @param log for writing to
* @param prog main program
* @param monitor TaskMonitor
* @param isAddToProgram whether we are importing the program into an existing program - this
* will always be false.
* @param programName name of program
*
* @return boolean success
*/
private boolean doImport(DecompileDebugFormatManager debugXmlMgr, List<Option> options,
MessageLog log, Program prog, TaskMonitor monitor, boolean isAddToProgram,
String programName)
throws IOException {
// if we're adding to another program, an autoAnalysisManager will already exist
if (!AutoAnalysisManager.hasAutoAnalysisManager(prog)) {
// helps to manage the threads/task workers
int txId = prog.startTransaction("Decompiler Debug XML Import");
try {
return doImportWork(debugXmlMgr, options, log, prog, monitor, isAddToProgram,
programName);
}
finally {
prog.endTransaction(txId, true);
}
}
AutoAnalysisManager analysisMgr = AutoAnalysisManager.getAnalysisManager(prog);
try {
return analysisMgr.scheduleWorker(new AnalysisWorker() {
@Override
public String getWorkerName() {
return "Decompiler Debug XML Importer";
}
@Override
public boolean analysisWorkerCallback(Program program, Object workerContext,
TaskMonitor taskMonitor) throws Exception, CancelledException {
return doImportWork(debugXmlMgr, options, log, program, taskMonitor,
isAddToProgram, programName);
}
}, null, false, monitor);
}
catch (CancelledException e) {
return false;
}
catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
}
throw new RuntimeException(e);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
/**
* Handles calling the parsing function in the DecompileDebugFormat manager.
*
* @param debugXmlMgr DecompileDebugFormatManager object which handles the parsing of the XML
* @param options if there are any settings for importing work - these will always be empty
* @param log for writing to
* @param prog main program
* @param monitor TaskMonitor
* @param isAddToProgram whether we are importing the program into an existing program - this
* will always be false.
* @param programName name of program
*
* @return success
*/
private boolean doImportWork(DecompileDebugFormatManager debugXmlMgr, List<Option> options,
MessageLog log, Program prog, TaskMonitor monitor, boolean isAddToProgram,
String programName)
throws LoadException {
MessageLog mgrLog = null;
boolean success = false;
try {
mgrLog = debugXmlMgr.read(prog, monitor, programName);
log.copyFrom(mgrLog);
success = true;
}
catch (Exception e) {
String message = "";
if (mgrLog != null && !"".equals(mgrLog.toString())) {
message = mgrLog.toString();
}
if (log != null && !"".equals(log.toString())) {
message = log.toString();
}
Msg.warn(this, "XML import exception, log: " + message, e);
throw new LoadException(e.getMessage());
}
return success;
}
/**
* Class to organize the XML format handler and the program info as extracted from the
* uploaded decompiler debug XML file.
*/
private static class ParseGhidraDebugResult {
final DecompileDebugFormatManager debugXmlMgr;
final DecompileDebugProgramInfo lastInfo;
ParseGhidraDebugResult(DecompileDebugFormatManager debugXmlMgr,
DecompileDebugProgramInfo lastInfo) {
this.debugXmlMgr = debugXmlMgr;
this.lastInfo = lastInfo;
}
}
/**
* Parse the XML file for the needed tags in order to populate a full program to load
*
* @param provider ByteProvider from the XML
*
* @return result object populated with the program details from the XML file
*/
private ParseGhidraDebugResult parse(ByteProvider provider) {
try {
DecompileDebugFormatManager debugXmlMgr = new DecompileDebugFormatManager(provider);
DecompileDebugProgramInfo decompileProgInfo = debugXmlMgr.getProgramInfo();
return new ParseGhidraDebugResult(debugXmlMgr, decompileProgInfo);
}
catch (Throwable e) {
Msg.trace(this, "Unable to parse the Ghidra Decompiler XML for " + provider.getName(),
e);
return new ParseGhidraDebugResult(null, null);
}
}
@Override
protected void loadProgramInto(ByteProvider provider, LoadSpec loadSpec, List<Option> options,
MessageLog messageLog, Program program, TaskMonitor monitor)
throws IOException, LoadException, CancelledException {
// since we will not ever be loading this debug program into an existing program, this
// should not be used.
}
/**
* Hold program details from the spec string for loading. Object is then passed forward in the
* .getProgramInfo() function implemented in @DecompileDebugFormatManager.java and populated by @DecompileDebugXmlLoader.java.
* @param offset String
* @param compilerString String
* @param specString String
*/
static record DecompileDebugProgramInfo(String offset, String compilerString,
String specString) {}
}
@@ -0,0 +1,239 @@
/* ###
* 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.opinion;
import static org.junit.Assert.*;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.AccessMode;
import java.util.*;
import org.junit.Before;
import org.junit.Test;
import org.xml.sax.SAXException;
import ghidra.app.util.Option;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.bin.FileByteProvider;
import ghidra.app.util.importer.MessageLog;
import ghidra.app.util.opinion.DecompileDebugXmlLoader.DecompileDebugProgramInfo;
import ghidra.framework.model.DomainObject;
import ghidra.program.database.ProgramDB;
import ghidra.program.database.data.ProgramDataTypeManager;
import ghidra.program.database.function.FunctionManagerDB;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.DataTypeComponent;
import ghidra.program.model.data.Structure;
import ghidra.program.model.listing.Function;
import ghidra.program.model.mem.Memory;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
import ghidra.util.task.TaskMonitor;
import resources.ResourceManager;
/**
*
*/
public class DecompileDebugXmlLoaderTest extends AbstractGhidraHeadedIntegrationTest {
File decompileDebugTestFile;
private DecompileDebugXmlLoader loader;
private ProgramDB program;
@Before
public void setUp() {
String DECOMPILE_DEBUG_TEST_FILE = "ghidra/app/util/opinion/decompile_debug_test.xml";
decompileDebugTestFile = ResourceManager.getResourceFile(DECOMPILE_DEBUG_TEST_FILE);
loader = new DecompileDebugXmlLoader();
}
/**
* Test method for {@link ghidra.app.util.opinion.DecompileDebugFormatManager#getProgramInfo()}.
*/
@Test
public void testDecompileDebugXmlLoad() {
Collection<LoadSpec> loadSpecs;
MessageLog log = new MessageLog();
try {
ByteProvider byteProvider =
new FileByteProvider(decompileDebugTestFile, null, AccessMode.READ);
loadSpecs = loader.findSupportedLoadSpecs(byteProvider);
assertTrue("Expected single loader opinion", loadSpecs.size() == 1);
LoadSpec loadSpec = loadSpecs.iterator().next();
List<Option> options = loader.getDefaultOptions(byteProvider, loadSpec, null, false);
LoadResults<? extends DomainObject> loadResults =
loader.load(byteProvider, byteProvider.getName(), null, null, loadSpec, options,
log, this, TaskMonitor.DUMMY);
loadResults.getNonPrimary();
program = (ProgramDB) loadResults.getPrimaryDomainObject(this);
assertTrue("expected single loaded program", loadResults.size() == 1);
}
catch (IOException | CancelledException | VersionException e) {
log.appendException(e);
}
}
@Test
public void testVerifyProgramInfo() {
DecompileDebugFormatManager mngr = new DecompileDebugFormatManager(decompileDebugTestFile);
DecompileDebugProgramInfo progInfo;
MessageLog log = new MessageLog();
try {
progInfo = mngr.getProgramInfo();
assertEquals("Spec string should be parsed correctly", "x86:LE:64:default",
progInfo.specString());
assertEquals("Compiler string should be extracted separately", "gcc",
progInfo.compilerString());
assertEquals("Memory offset should be parsed from the offset tag", "0x140022cb8",
progInfo.offset());
}
catch (SAXException | IOException e) {
log.appendException(e);
}
}
@Test
public void testVerifyLoadedProgramBytes() {
Collection<LoadSpec> loadSpecs;
MessageLog log = new MessageLog();
try {
ByteProvider byteProvider =
new FileByteProvider(decompileDebugTestFile, null, AccessMode.READ);
loadSpecs = loader.findSupportedLoadSpecs(byteProvider);
LoadSpec loadSpec = loadSpecs.iterator().next();
List<Option> options = loader.getDefaultOptions(byteProvider, loadSpec, null, false);
LoadResults<? extends DomainObject> loadResults =
loader.load(byteProvider, byteProvider.getName(), null, null, loadSpec, options,
new MessageLog(), this, TaskMonitor.DUMMY);
loadResults.getNonPrimary();
program = (ProgramDB) loadResults.getPrimaryDomainObject(this);
assertEquals("gcc", program.getCompilerSpec().getCompilerSpecID().toString());
Memory memory = program.getMemory();
MemoryBlock[] blocks = memory.getBlocks();
assertEquals(26, blocks.length);
// Verify memory blocks
verifyBlock(blocks[0], "decompile_debug_test.xml", true,
getAddr(new BigInteger("140000000", 16)), 128);
verifyBlock(blocks[1], "__scrt_acquire_startup_lock", true,
getAddr(new BigInteger("1400227f8", 16)), 1);
verifyBlock(blocks[2], "FUN_140022834", true,
getAddr(new BigInteger("140022834", 16)), 1);
verifyBlock(blocks[3], "FUN_1400228fc", true,
getAddr(new BigInteger("1400228fc", 16)), 1);
verifyBlock(blocks[4], "__scrt_release_startup_lock", true,
getAddr(new BigInteger("140022994", 16)), 1);
verifyBlock(blocks[5], "__scrt_uninitialize_crt", true,
getAddr(new BigInteger("1400229b8", 16)), 1);
verifyBlock(blocks[6], "decompile_debug_test.xml", true,
getAddr(new BigInteger("140022cb8", 16)), 296);
verifyBlock(blocks[7], "decompile_debug_test.xml", true,
getAddr(new BigInteger("140022df9", 16)), 39);
}
catch (IOException | CancelledException | VersionException e) {
log.appendException(e);
}
}
@Test
public void testVerifyLoadedProgramDataTypes() {
Collection<LoadSpec> loadSpecs;
MessageLog log = new MessageLog();
try {
ByteProvider byteProvider =
new FileByteProvider(decompileDebugTestFile, null, AccessMode.READ);
loadSpecs = loader.findSupportedLoadSpecs(byteProvider);
LoadSpec loadSpec = loadSpecs.iterator().next();
List<Option> options = loader.getDefaultOptions(byteProvider, loadSpec, null, false);
LoadResults<? extends DomainObject> loadResults =
loader.load(byteProvider, byteProvider.getName(), null, null, loadSpec, options,
new MessageLog(), this, TaskMonitor.DUMMY);
loadResults.getNonPrimary();
program = (ProgramDB) loadResults.getPrimaryDomainObject(this);
// Verify data type generation
ProgramDataTypeManager dtm = program.getDataTypeManager();
assertEquals("Category count didn't match. ", 2, dtm.getCategoryCount());
Iterator<Structure> structures = dtm.getAllStructures();
Structure struct = structures.next(); // there is only 1 struct in the example XML dump
assertEquals("Component count didn't match.", 20, struct.getNumComponents());
assertEquals("Struct name is incorrect", "IMAGE_DOS_HEADER", struct.getName());
DataTypeComponent array = struct.getComponentAt(0); // the first component is an array
assertEquals("Array component name doesn't match", "e_magic", array.getFieldName());
assertEquals("Array wasn't sized right", 2, array.getLength());
}
catch (IOException | CancelledException | VersionException e) {
log.appendException(e);
}
}
@Test
public void verifyLoadedProgramFunctions() {
Collection<LoadSpec> loadSpecs;
MessageLog log = new MessageLog();
try {
ByteProvider byteProvider =
new FileByteProvider(decompileDebugTestFile, null, AccessMode.READ);
loadSpecs = loader.findSupportedLoadSpecs(byteProvider);
LoadSpec loadSpec = loadSpecs.iterator().next();
List<Option> options = loader.getDefaultOptions(byteProvider, loadSpec, null, false);
LoadResults<? extends DomainObject> loadResults =
loader.load(byteProvider, byteProvider.getName(), null, null, loadSpec, options,
new MessageLog(), this, TaskMonitor.DUMMY);
loadResults.getNonPrimary();
program = (ProgramDB) loadResults.getPrimaryDomainObject(this);
// Verify function collection
FunctionManagerDB funcMngr = program.getFunctionManager();
assertEquals("Function count doesn't match", 19, funcMngr.getFunctionCount());
Iterator<Function> functions = funcMngr.getFunctions(true);
Function function = functions.next(); // this is the function the dump was made for; the others are references
assertEquals("Function name needs to match.", "__scrt_acquire_startup_lock",
function.getName());
}
catch (IOException | CancelledException | VersionException e) {
log.appendException(e);
}
}
private void verifyBlock(MemoryBlock block, String name, boolean initialized, Address min,
long length) {
assertEquals("Name should match", name, block.getName());
assertEquals("Initalization didn't match", initialized, block.isInitialized());
assertEquals("Start address didn't match", min, block.getStart());
assertEquals("Block length didn't match", length, block.getSize());
}
private Address getAddr(BigInteger offset) {
return program.getAddressFactory().getDefaultAddressSpace().getAddress(offset.longValue());
}
}
@@ -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,4 +44,6 @@ public interface XmlElement {
public int getLineNumber();
public void setAttribute(String key, String value);
public boolean isStart(String string);
}
@@ -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.
@@ -189,4 +189,16 @@ public class XmlElementImpl implements XmlElement {
}
return result;
}
@Override
/**
* Checks that the element is a starting element and also that its name matches the passed
* string.
*
* @param string element name to check for a match.
* @return boolean
*/
public boolean isStart(String string) {
return this.isStart && this.name.equals(string);
}
}