diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultSchemaContext.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultSchemaContext.java index fbc0bfb61e..b733bbd989 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultSchemaContext.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultSchemaContext.java @@ -70,12 +70,10 @@ public class DefaultSchemaContext implements SchemaContext { @Override public boolean equals(Object obj) { - if (obj instanceof DefaultSchemaContext) { - DefaultSchemaContext that = (DefaultSchemaContext) obj; + if (obj instanceof DefaultSchemaContext that) { return Objects.equals(this.schemas, that.schemas); } - if (obj instanceof SchemaContext) { - SchemaContext that = (SchemaContext) obj; + if (obj instanceof SchemaContext that) { return this.schemas.values().equals(that.getAllSchemas()); } return false; diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultTargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultTargetObjectSchema.java index bd714d1835..21221412ff 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultTargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/DefaultTargetObjectSchema.java @@ -61,10 +61,9 @@ public class DefaultTargetObjectSchema */ @Override public boolean equals(Object obj) { - if (!(obj instanceof DefaultAttributeSchema)) { + if (!(obj instanceof DefaultAttributeSchema that)) { return false; } - DefaultAttributeSchema that = (DefaultAttributeSchema) obj; if (!Objects.equals(this.name, that.name)) { return false; } @@ -119,6 +118,64 @@ public class DefaultTargetObjectSchema } } + protected static class AliasResolver { + private final Map schemas; + private final Map aliases; + private final AttributeSchema defaultSchema; + private Map resolvedAliases; + + public AliasResolver(Map schemas, Map aliases, + AttributeSchema defaultSchema) { + this.schemas = schemas; + this.aliases = aliases; + this.defaultSchema = defaultSchema; + } + + public Map resolveAliases() { + this.resolvedAliases = new LinkedHashMap<>(); + for (String alias : aliases.keySet()) { + if (alias.equals("")) { + throw new IllegalArgumentException("Key '' cannot be an alias"); + } + if (schemas.containsKey(alias)) { + throw new IllegalArgumentException( + "Key '%s' cannot be both an attribute and an alias".formatted(alias)); + } + resolveAlias(alias, new LinkedHashSet<>()); + } + return resolvedAliases; + } + + protected String resolveAlias(String alias, LinkedHashSet visited) { + String already = resolvedAliases.get(alias); + if (already != null) { + return already; + } + if (!visited.add(alias)) { + throw new IllegalArgumentException("Cycle of aliases: " + visited); + } + String to = aliases.get(alias); + if (to == null) { + return alias; + } + if (to.equals("")) { + throw new IllegalArgumentException( + "Cannot alias to key '' (from %s)".formatted(alias)); + } + String result = resolveAlias(to, visited); + resolvedAliases.put(alias, result); + return result; + } + + public Map resolveSchemas() { + Map resolved = new LinkedHashMap<>(schemas); + for (Map.Entry ent : resolvedAliases.entrySet()) { + resolved.put(ent.getKey(), schemas.getOrDefault(ent.getValue(), defaultSchema)); + } + return resolved; + } + } + private final SchemaContext context; private final SchemaName name; private final Class type; @@ -130,6 +187,7 @@ public class DefaultTargetObjectSchema private final ResyncMode elementResync; private final Map attributeSchemas; + private final Map attributeAliases; private final AttributeSchema defaultAttributeSchema; private final ResyncMode attributeResync; @@ -137,7 +195,8 @@ public class DefaultTargetObjectSchema Set> interfaces, boolean isCanonicalContainer, Map elementSchemas, SchemaName defaultElementSchema, ResyncMode elementResync, - Map attributeSchemas, AttributeSchema defaultAttributeSchema, + Map attributeSchemas, Map attributeAliases, + AttributeSchema defaultAttributeSchema, ResyncMode attributeResync) { this.context = context; this.name = name; @@ -149,7 +208,10 @@ public class DefaultTargetObjectSchema this.defaultElementSchema = defaultElementSchema; this.elementResync = elementResync; - this.attributeSchemas = Collections.unmodifiableMap(new LinkedHashMap<>(attributeSchemas)); + AliasResolver resolver = + new AliasResolver(attributeSchemas, attributeAliases, defaultAttributeSchema); + this.attributeAliases = Collections.unmodifiableMap(resolver.resolveAliases()); + this.attributeSchemas = Collections.unmodifiableMap(resolver.resolveSchemas()); this.defaultAttributeSchema = defaultAttributeSchema; this.attributeResync = attributeResync; } @@ -199,6 +261,11 @@ public class DefaultTargetObjectSchema return attributeSchemas; } + @Override + public Map getAttributeAliases() { + return attributeAliases; + } + @Override public AttributeSchema getDefaultAttributeSchema() { return defaultAttributeSchema; @@ -236,6 +303,7 @@ public class DefaultTargetObjectSchema sb.append("attributes(resync " + attributeResync + ") = "); sb.append(attributeSchemas); sb.append(" default " + defaultAttributeSchema); + sb.append(" aliases " + attributeAliases); sb.append("\n}"); } @@ -255,10 +323,9 @@ public class DefaultTargetObjectSchema */ @Override public boolean equals(Object obj) { - if (!(obj instanceof DefaultTargetObjectSchema)) { + if (!(obj instanceof DefaultTargetObjectSchema that)) { return false; } - DefaultTargetObjectSchema that = (DefaultTargetObjectSchema) obj; if (!Objects.equals(this.name, that.name)) { return false; } @@ -283,6 +350,9 @@ public class DefaultTargetObjectSchema if (!Objects.equals(this.attributeSchemas, that.attributeSchemas)) { return false; } + if (!Objects.equals(this.attributeAliases, that.attributeAliases)) { + return false; + } if (!Objects.equals(this.defaultAttributeSchema, that.defaultAttributeSchema)) { return false; } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java index a4aa50d1a8..80bf2bc981 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/EnumerableTargetObjectSchema.java @@ -191,6 +191,11 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema { return Map.of(); } + @Override + public Map getAttributeAliases() { + return Map.of(); + } + @Override public AttributeSchema getDefaultAttributeSchema() { return AttributeSchema.DEFAULT_VOID; diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/SchemaBuilder.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/SchemaBuilder.java index b7bc02d4a0..a1498eae64 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/SchemaBuilder.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/SchemaBuilder.java @@ -33,6 +33,7 @@ public class SchemaBuilder { private ResyncMode elementResync = TargetObjectSchema.DEFAULT_ELEMENT_RESYNC; private Map attributeSchemas = new LinkedHashMap<>(); + private Map attributeAliases = new LinkedHashMap<>(); private AttributeSchema defaultAttributeSchema = AttributeSchema.DEFAULT_ANY; private ResyncMode attributeResync = TargetObjectSchema.DEFAULT_ATTRIBUTE_RESYNC; @@ -163,10 +164,10 @@ public class SchemaBuilder { if (schema.getName().equals("")) { return setDefaultAttributeSchema(schema); } - if (attributeSchemas.containsKey(schema.getName())) { - throw new IllegalArgumentException("Duplicate attribute name '" + schema.getName() + - "' origin1=" + attributeOrigins.get(schema.getName()) + - " origin2=" + origin); + if (attributeOrigins.containsKey(schema.getName())) { + throw new IllegalArgumentException( + "Duplicate attribute name '%s' adding schema origin1=%s origin2=%s".formatted( + schema.getName(), attributeOrigins.get(schema.getName()), origin)); } attributeSchemas.put(schema.getName(), schema); attributeOrigins.put(schema.getName(), origin); @@ -178,6 +179,7 @@ public class SchemaBuilder { return setDefaultAttributeSchema(AttributeSchema.DEFAULT_ANY); } attributeSchemas.remove(name); + attributeAliases.remove(name); attributeOrigins.remove(name); return this; } @@ -194,11 +196,41 @@ public class SchemaBuilder { if (schema.getName().equals("")) { return setDefaultAttributeSchema(schema); } + attributeAliases.remove(schema.getName()); attributeSchemas.put(schema.getName(), schema); attributeOrigins.put(schema.getName(), origin); return this; } + protected void validateAlias(String from, String to) { + if (from.equals("")) { + throw new IllegalArgumentException("Key '' cannot be an alias"); + } + if (to.equals("")) { + throw new IllegalArgumentException("Cannot alias to key '' (from %s)".formatted(from)); + } + } + + public SchemaBuilder addAttributeAlias(String from, String to, Object origin) { + validateAlias(from, to); + if (attributeOrigins.containsKey(from)) { + throw new IllegalArgumentException( + "Duplicate attribute name '%s' adding alias origin1=%s origin2=%s".formatted( + from, attributeOrigins.get(from), origin)); + } + attributeAliases.put(from, to); + attributeOrigins.put(from, origin); + return this; + } + + public SchemaBuilder replaceAttributeAlias(String from, String to, Object origin) { + validateAlias(from, to); + attributeSchemas.remove(from); + attributeAliases.put(from, to); + attributeOrigins.put(from, origin); + return this; + } + public SchemaBuilder setDefaultAttributeSchema(AttributeSchema defaultAttributeSchema) { this.defaultAttributeSchema = defaultAttributeSchema; return this; @@ -227,6 +259,6 @@ public class SchemaBuilder { return new DefaultTargetObjectSchema( context, name, type, interfaces, isCanonicalContainer, elementSchemas, defaultElementSchema, elementResync, - attributeSchemas, defaultAttributeSchema, attributeResync); + attributeSchemas, attributeAliases, defaultAttributeSchema, attributeResync); } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java index 4e57f85fc1..d5a9d0340c 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/TargetObjectSchema.java @@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; import java.util.stream.Collectors; +import ghidra.dbg.DebuggerObjectModel.RefreshBehavior; import ghidra.dbg.agent.DefaultTargetObject; import ghidra.dbg.target.*; import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema; @@ -42,6 +43,11 @@ import ghidra.util.Msg; * by matching on the keys (indices and names), the result being a subordinate * {@link TargetObjectSchema}. Keys must match exactly, unless the "pattern" is the empty string, * which matches any key. Similarly, the wild-card index is {@code []}. + * + *

+ * The schema can specify attribute aliases, which implies that a particular key ("from") will + * always have the same value as another ("to"). As a result, the schemas of aliased keys will also + * implicitly match. */ public interface TargetObjectSchema { public static final ResyncMode DEFAULT_ELEMENT_RESYNC = ResyncMode.NEVER; @@ -107,9 +113,9 @@ public interface TargetObjectSchema { * *

* Each object specifies a element sync mode, and an attribute sync mode. These describe when - * the client must call {@link TargetObject#resync(boolean, boolean)} to refresh/resync to - * ensure it has a fresh cache of elements and/or attributes. Note that any client requesting a - * resync will cause all clients to receive the updates. + * the client must call {@link TargetObject#resync(RefreshBehavior, RefreshBehavior)} to + * refresh/resync to ensure it has a fresh cache of elements and/or attributes. Note that any + * client requesting a resync will cause all clients to receive the updates. */ enum ResyncMode { /** @@ -332,10 +338,42 @@ public interface TargetObjectSchema { /** * Get the map of attribute names to named schemas * + *

+ * The returned map will include aliases. To determine whether or not an attribute key is an + * alias, check whether the entry's key matches the name of the attribute (see + * {@link AttributeSchema#getName()}). It is possible the schema's name is empty, i.e., the + * default schema. This indicates an alias to a key that was not named in the schema. Use + * {@link #getAttributeAliases()} to determine the name of that key. + * * @return the map */ Map getAttributeSchemas(); + /** + * Get the map of attribute name aliases + * + *

+ * The returned map must provide the direct alias names. For any given key, the client + * need only query the map once to determine the name of the attribute to which the alias + * refers. Consequently, the map also cannot indicate a cycle. + * + *

+ * An aliased attribute takes the value of its target implicitly. + * + * @return the map + */ + Map getAttributeAliases(); + + /** + * Check if the given name is an alias and get the target attribute name + * + * @param name the name + * @return the alias' target, or the given name if not an alias + */ + default String checkAliasedAttribute(String name) { + return getAttributeAliases().getOrDefault(name, name); + } + /** * Get the default schema for attributes * @@ -355,8 +393,8 @@ public interface TargetObjectSchema { * Get the attribute schema for a given attribute name * *

- * If there's a schema specified for the given name, that schema is taken. Otherwise, the - * default attribute schema is taken. + * If there's a schema specified for the given name, that schema is taken. If the name refers to + * an alias, its schema is taken. Otherwise, the default attribute schema is taken. * * @param name the name * @return the attribute schema @@ -824,7 +862,7 @@ public interface TargetObjectSchema { * * @param type * @param path - * @return + * @return the predicates for finding objects */ default PathPredicates matcherForSuitable(Class type, List path) { @@ -947,7 +985,10 @@ public interface TargetObjectSchema { * Verify that the given value is of this schema's required type and, if applicable, implements * the required interfaces * - * @param value the value + * @param value the value being assigned to the key + * @param parentPath the path of the object whose key is being assigned, for diagnostics + * @param key the key that is being assigned + * @param strict true to throw an exception upon violation; false to just log and continue */ default void validateTypeAndInterfaces(Object value, List parentPath, String key, boolean strict) { diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/XmlSchemaContext.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/XmlSchemaContext.java index c935a2fc07..c0d4f0109a 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/XmlSchemaContext.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/target/schema/XmlSchemaContext.java @@ -29,7 +29,25 @@ import ghidra.util.Msg; import ghidra.util.xml.XmlUtilities; public class XmlSchemaContext extends DefaultSchemaContext { - protected static final Set TRUES = Set.of("true", "yes", "y", "1"); + protected static final String ELEM_CONTEXT = "context"; + protected static final String ATTR_CANONICAL = "canonical"; + protected static final String ELEM_SCHEMA = "schema"; + protected static final String ATTR_ELEMENT_RESYNC = "elementResync"; + protected static final String ATTR_ATTRIBUTE_RESYNC = "attributeResync"; + protected static final String ELEM_INTERFACE = "interface"; + protected static final String ELEM_ELEMENT = "element"; + protected static final String ATTR_INDEX = "index"; + protected static final String ELEM_ATTRIBUTE = "attribute"; + protected static final String ATTR_NAME = "name"; + protected static final String ATTR_SCHEMA = "schema"; + protected static final String ATTR_REQUIRED = "required"; + protected static final String ATTR_FIXED = "fixed"; + protected static final String ATTR_HIDDEN = "hidden"; + protected static final String ELEM_ATTRIBUTE_ALIAS = "attribute-alias"; + protected static final String ATTR_FROM = "from"; + protected static final String ATTR_TO = "to"; + protected static final String YES = "yes"; + protected static final Set TRUES = Set.of("true", YES, "y", "1"); protected static boolean parseBoolean(Element ele, String attrName) { return TRUES.contains(ele.getAttributeValue(attrName, "no").toLowerCase()); @@ -40,7 +58,7 @@ public class XmlSchemaContext extends DefaultSchemaContext { } public static Element contextToXml(SchemaContext ctx) { - Element result = new Element("context"); + Element result = new Element(ELEM_CONTEXT); for (TargetObjectSchema schema : ctx.getAllSchemas()) { Element schemaElem = schemaToXml(schema); if (schemaElem != null) { @@ -51,23 +69,30 @@ public class XmlSchemaContext extends DefaultSchemaContext { } public static Element attributeSchemaToXml(AttributeSchema as) { - Element attrElem = new Element("attribute"); + Element attrElem = new Element(ELEM_ATTRIBUTE); if (!as.getName().equals("")) { - XmlUtilities.setStringAttr(attrElem, "name", as.getName()); + XmlUtilities.setStringAttr(attrElem, ATTR_NAME, as.getName()); } - XmlUtilities.setStringAttr(attrElem, "schema", as.getSchema().toString()); + XmlUtilities.setStringAttr(attrElem, ATTR_SCHEMA, as.getSchema().toString()); if (as.isRequired()) { - XmlUtilities.setStringAttr(attrElem, "required", "yes"); + XmlUtilities.setStringAttr(attrElem, ATTR_REQUIRED, YES); } if (as.isFixed()) { - XmlUtilities.setStringAttr(attrElem, "fixed", "yes"); + XmlUtilities.setStringAttr(attrElem, ATTR_FIXED, YES); } if (as.isHidden()) { - XmlUtilities.setStringAttr(attrElem, "hidden", "yes"); + XmlUtilities.setStringAttr(attrElem, ATTR_HIDDEN, YES); } return attrElem; } + public static Element aliasToXml(Map.Entry alias) { + Element aliasElem = new Element(ELEM_ATTRIBUTE_ALIAS); + XmlUtilities.setStringAttr(aliasElem, ATTR_FROM, alias.getKey()); + XmlUtilities.setStringAttr(aliasElem, ATTR_TO, alias.getValue()); + return aliasElem; + } + public static Element schemaToXml(TargetObjectSchema schema) { if (!TargetObject.class.isAssignableFrom(schema.getType())) { return null; @@ -76,33 +101,40 @@ public class XmlSchemaContext extends DefaultSchemaContext { return null; } - Element result = new Element("schema"); - XmlUtilities.setStringAttr(result, "name", schema.getName().toString()); + Element result = new Element(ELEM_SCHEMA); + XmlUtilities.setStringAttr(result, ATTR_NAME, schema.getName().toString()); for (Class iface : schema.getInterfaces()) { - Element ifElem = new Element("interface"); - XmlUtilities.setStringAttr(ifElem, "name", DebuggerObjectModel.requireIfaceName(iface)); + Element ifElem = new Element(ELEM_INTERFACE); + XmlUtilities.setStringAttr(ifElem, ATTR_NAME, + DebuggerObjectModel.requireIfaceName(iface)); result.addContent(ifElem); } if (schema.isCanonicalContainer()) { - XmlUtilities.setStringAttr(result, "canonical", "yes"); + XmlUtilities.setStringAttr(result, ATTR_CANONICAL, YES); } - XmlUtilities.setStringAttr(result, "elementResync", + XmlUtilities.setStringAttr(result, ATTR_ELEMENT_RESYNC, schema.getElementResyncMode().name()); - XmlUtilities.setStringAttr(result, "attributeResync", + XmlUtilities.setStringAttr(result, ATTR_ATTRIBUTE_RESYNC, schema.getAttributeResyncMode().name()); for (Map.Entry ent : schema.getElementSchemas().entrySet()) { - Element elemElem = new Element("element"); - XmlUtilities.setStringAttr(elemElem, "index", ent.getKey()); - XmlUtilities.setStringAttr(elemElem, "schema", ent.getValue().toString()); + Element elemElem = new Element(ELEM_ELEMENT); + XmlUtilities.setStringAttr(elemElem, ATTR_INDEX, ent.getKey()); + XmlUtilities.setStringAttr(elemElem, ATTR_SCHEMA, ent.getValue().toString()); result.addContent(elemElem); } - Element deElem = new Element("element"); - XmlUtilities.setStringAttr(deElem, "schema", schema.getDefaultElementSchema().toString()); + Element deElem = new Element(ELEM_ELEMENT); + XmlUtilities.setStringAttr(deElem, ATTR_SCHEMA, + schema.getDefaultElementSchema().toString()); result.addContent(deElem); - for (AttributeSchema as : schema.getAttributeSchemas().values()) { + for (Map.Entry ent : schema.getAttributeSchemas().entrySet()) { + AttributeSchema as = ent.getValue(); + if (!ent.getKey().equals(as.getName())) { + // Exclude aliases here + continue; + } Element attrElem = attributeSchemaToXml(as); result.addContent(attrElem); } @@ -110,6 +142,12 @@ public class XmlSchemaContext extends DefaultSchemaContext { Element daElem = attributeSchemaToXml(das); result.addContent(daElem); + // Yes, these will be the "resolved" aliases, but I think that's okay. + for (Map.Entry alias : schema.getAttributeAliases().entrySet()) { + Element aliasElem = aliasToXml(alias); + result.addContent(aliasElem); + } + return result; } @@ -138,7 +176,7 @@ public class XmlSchemaContext extends DefaultSchemaContext { public static XmlSchemaContext contextFromXml(Element contextElem) { XmlSchemaContext ctx = new XmlSchemaContext(); - for (Element schemaElem : XmlUtilities.getChildren(contextElem, "schema")) { + for (Element schemaElem : XmlUtilities.getChildren(contextElem, ELEM_SCHEMA)) { ctx.schemaFromXml(schemaElem); } return ctx; @@ -153,16 +191,17 @@ public class XmlSchemaContext extends DefaultSchemaContext { private String requireAttributeValue(Element elem, String name) { String value = elem.getAttributeValue(name); if (value == null) { - throw new IllegalArgumentException("Missing attribute " + name + " in " + elem); + throw new IllegalArgumentException( + "Missing attribute '%s' in %s".formatted(name, elem)); } return value; } public TargetObjectSchema schemaFromXml(Element schemaElem) { - SchemaBuilder builder = builder(name(schemaElem.getAttributeValue("name", ""))); + SchemaBuilder builder = builder(name(schemaElem.getAttributeValue(ATTR_NAME, ""))); - for (Element ifaceElem : XmlUtilities.getChildren(schemaElem, "interface")) { - String ifaceName = requireAttributeValue(ifaceElem, "name"); + for (Element ifaceElem : XmlUtilities.getChildren(schemaElem, ELEM_INTERFACE)) { + String ifaceName = requireAttributeValue(ifaceElem, ATTR_NAME); Class iface = TargetObject.INTERFACES_BY_NAME.get(ifaceName); if (iface == null) { Msg.warn(this, "Unknown interface name: '" + ifaceName + "'"); @@ -172,29 +211,35 @@ public class XmlSchemaContext extends DefaultSchemaContext { } } - builder.setCanonicalContainer(parseBoolean(schemaElem, "canonical")); + builder.setCanonicalContainer(parseBoolean(schemaElem, ATTR_CANONICAL)); builder.setElementResyncMode( - ResyncMode.valueOf(requireAttributeValue(schemaElem, "elementResync"))); + ResyncMode.valueOf(requireAttributeValue(schemaElem, ATTR_ELEMENT_RESYNC))); builder.setAttributeResyncMode( - ResyncMode.valueOf(requireAttributeValue(schemaElem, "attributeResync"))); + ResyncMode.valueOf(requireAttributeValue(schemaElem, ATTR_ATTRIBUTE_RESYNC))); - for (Element elemElem : XmlUtilities.getChildren(schemaElem, "element")) { - SchemaName schema = name(requireAttributeValue(elemElem, "schema")); - String index = elemElem.getAttributeValue("index", ""); + for (Element elemElem : XmlUtilities.getChildren(schemaElem, ELEM_ELEMENT)) { + SchemaName schema = name(requireAttributeValue(elemElem, ATTR_SCHEMA)); + String index = elemElem.getAttributeValue(ATTR_INDEX, ""); builder.addElementSchema(index, schema, elemElem); } - for (Element attrElem : XmlUtilities.getChildren(schemaElem, "attribute")) { - SchemaName schema = name(requireAttributeValue(attrElem, "schema")); - boolean required = parseBoolean(attrElem, "required"); - boolean fixed = parseBoolean(attrElem, "fixed"); - boolean hidden = parseBoolean(attrElem, "hidden"); + for (Element attrElem : XmlUtilities.getChildren(schemaElem, ELEM_ATTRIBUTE)) { + SchemaName schema = name(requireAttributeValue(attrElem, ATTR_SCHEMA)); + boolean required = parseBoolean(attrElem, ATTR_REQUIRED); + boolean fixed = parseBoolean(attrElem, ATTR_FIXED); + boolean hidden = parseBoolean(attrElem, ATTR_HIDDEN); - String name = attrElem.getAttributeValue("name", ""); + String name = attrElem.getAttributeValue(ATTR_NAME, ""); builder.addAttributeSchema( new DefaultAttributeSchema(name, schema, required, fixed, hidden), attrElem); } + for (Element aliasElem : XmlUtilities.getChildren(schemaElem, ELEM_ATTRIBUTE_ALIAS)) { + String from = requireAttributeValue(aliasElem, ATTR_FROM); + String to = requireAttributeValue(aliasElem, ATTR_TO); + builder.addAttributeAlias(from, to, aliasElem); + } + return builder.buildAndAdd(); } } diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/XmlTargetObjectSchemaTest.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/XmlTargetObjectSchemaTest.java index b07128631d..21ff1a2f9b 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/XmlTargetObjectSchemaTest.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/target/schema/XmlTargetObjectSchemaTest.java @@ -28,22 +28,25 @@ import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema import ghidra.dbg.target.schema.TargetObjectSchema.*; public class XmlTargetObjectSchemaTest { - protected static final String SCHEMA_XML = "" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - ""; + protected static final String SCHEMA_XML = + // Do not line-wrap or serialize test will fail + """ + + + + + + + + + + + + """; // Cannot have final final new-line or serialize test will fail protected static final DefaultSchemaContext CTX = new DefaultSchemaContext(); protected static final SchemaName NAME_ROOT = new SchemaName("root"); @@ -59,6 +62,7 @@ public class XmlTargetObjectSchemaTest { EnumerableTargetObjectSchema.INT.getName(), false, false, false), null) .addAttributeSchema(new DefaultAttributeSchema("some_object", EnumerableTargetObjectSchema.OBJECT.getName(), true, true, true), null) + .addAttributeAlias("_int", "some_int", null) .setAttributeResyncMode(ResyncMode.ONCE) .buildAndAdd(); protected static final TargetObjectSchema SCHEMA_DOWN1 = CTX.builder(NAME_DOWN1) diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointLocation.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointLocation.java index f96c362127..c86b77cd60 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointLocation.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointLocation.java @@ -40,9 +40,22 @@ import ghidra.util.exception.DuplicateNameException; public class DBTraceObjectBreakpointLocation implements TraceObjectBreakpointLocation, DBTraceObjectInterface { - protected class BreakpointChangeTranslator extends Translator { + protected static class BreakpointChangeTranslator extends Translator { + private static final Map> KEYS_BY_SCHEMA = + new WeakHashMap<>(); + + private final Set keys; + protected BreakpointChangeTranslator(DBTraceObject object, TraceBreakpoint iface) { super(TargetBreakpointLocation.RANGE_ATTRIBUTE_NAME, object, iface); + TargetObjectSchema schema = object.getTargetSchema(); + synchronized (KEYS_BY_SCHEMA) { + keys = KEYS_BY_SCHEMA.computeIfAbsent(schema, s -> Set.of( + schema.checkAliasedAttribute(TargetBreakpointLocation.RANGE_ATTRIBUTE_NAME), + schema.checkAliasedAttribute(TargetObject.DISPLAY_ATTRIBUTE_NAME), + schema.checkAliasedAttribute(TargetTogglable.ENABLED_ATTRIBUTE_NAME), + schema.checkAliasedAttribute(KEY_COMMENT))); + } } @Override @@ -62,10 +75,7 @@ public class DBTraceObjectBreakpointLocation @Override protected boolean appliesToKey(String key) { - return TargetBreakpointLocation.RANGE_ATTRIBUTE_NAME.equals(key) || - TargetObject.DISPLAY_ATTRIBUTE_NAME.equals(key) || - TargetBreakpointSpec.ENABLED_ATTRIBUTE_NAME.equals(key) || - KEY_COMMENT.equals(key); + return keys.contains(key); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointSpec.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointSpec.java index bb59a4bd9d..b168932050 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointSpec.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/breakpoint/DBTraceObjectBreakpointSpec.java @@ -15,12 +15,11 @@ */ package ghidra.trace.database.breakpoint; -import java.util.Collection; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; -import ghidra.dbg.target.TargetBreakpointSpec; -import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.*; +import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressRange; import ghidra.trace.database.target.DBTraceObject; @@ -44,12 +43,21 @@ import ghidra.util.exception.DuplicateNameException; public class DBTraceObjectBreakpointSpec implements TraceObjectBreakpointSpec, DBTraceObjectInterface { + private static final Map> KEYS_BY_SCHEMA = new WeakHashMap<>(); + private final DBTraceObject object; + private final Set keys; private TraceBreakpointKindSet kinds = TraceBreakpointKindSet.of(); public DBTraceObjectBreakpointSpec(DBTraceObject object) { this.object = object; + TargetObjectSchema schema = object.getTargetSchema(); + synchronized (KEYS_BY_SCHEMA) { + keys = KEYS_BY_SCHEMA.computeIfAbsent(schema, s -> Set.of( + schema.checkAliasedAttribute(TargetBreakpointSpec.KINDS_ATTRIBUTE_NAME), + schema.checkAliasedAttribute(TargetTogglable.ENABLED_ATTRIBUTE_NAME))); + } } @Override @@ -238,8 +246,7 @@ public class DBTraceObjectBreakpointSpec TraceObjectChangeType.VALUE_CREATED.cast(rec); TraceObjectValue affected = cast.getAffectedObject(); String key = affected.getEntryKey(); - boolean applies = TargetBreakpointSpec.KINDS_ATTRIBUTE_NAME.equals(key) || - TargetBreakpointSpec.ENABLED_ATTRIBUTE_NAME.equals(key); + boolean applies = keys.contains(key); if (!applies) { return null; } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceObjectMemoryRegion.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceObjectMemoryRegion.java index b84f1e1e9a..035c257bbf 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceObjectMemoryRegion.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceObjectMemoryRegion.java @@ -19,6 +19,7 @@ import java.util.*; import ghidra.dbg.target.TargetMemoryRegion; import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.program.model.address.*; import ghidra.trace.database.DBTrace; import ghidra.trace.database.DBTraceUtils; @@ -38,9 +39,46 @@ import ghidra.util.exception.DuplicateNameException; public class DBTraceObjectMemoryRegion implements TraceObjectMemoryRegion, DBTraceObjectInterface { + protected record Keys(Set all, String range, String display, + Set flags) { + static Keys fromSchema(TargetObjectSchema schema) { + String keyRange = schema.checkAliasedAttribute(TargetMemoryRegion.RANGE_ATTRIBUTE_NAME); + String keyDisplay = schema.checkAliasedAttribute(TargetObject.DISPLAY_ATTRIBUTE_NAME); + String keyReadable = + schema.checkAliasedAttribute(TargetMemoryRegion.READABLE_ATTRIBUTE_NAME); + String keyWritable = + schema.checkAliasedAttribute(TargetMemoryRegion.WRITABLE_ATTRIBUTE_NAME); + String keyExecutable = + schema.checkAliasedAttribute(TargetMemoryRegion.EXECUTABLE_ATTRIBUTE_NAME); + return new Keys(Set.of(keyRange, keyDisplay, keyReadable, keyWritable, keyExecutable), + keyRange, keyDisplay, Set.of(keyReadable, keyWritable, keyExecutable)); + } + + public boolean isRange(String key) { + return range.equals(key); + } + + public boolean isDisplay(String key) { + return display.equals(key); + } + + public boolean isFlag(String key) { + return flags.contains(key); + } + } + protected class RegionChangeTranslator extends Translator { + private static final Map KEYS_BY_SCHEMA = + new WeakHashMap<>(); + + private final Keys keys; + protected RegionChangeTranslator(DBTraceObject object, TraceMemoryRegion iface) { super(TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, object, iface); + TargetObjectSchema schema = object.getTargetSchema(); + synchronized (KEYS_BY_SCHEMA) { + keys = KEYS_BY_SCHEMA.computeIfAbsent(schema, Keys::fromSchema); + } } @Override @@ -60,11 +98,7 @@ public class DBTraceObjectMemoryRegion implements TraceObjectMemoryRegion, DBTra @Override protected boolean appliesToKey(String key) { - return TargetMemoryRegion.RANGE_ATTRIBUTE_NAME.equals(key) || - TargetObject.DISPLAY_ATTRIBUTE_NAME.equals(key) || - TargetMemoryRegion.READABLE_ATTRIBUTE_NAME.equals(key) || - TargetMemoryRegion.WRITABLE_ATTRIBUTE_NAME.equals(key) || - TargetMemoryRegion.EXECUTABLE_ATTRIBUTE_NAME.equals(key); + return keys.all.contains(key); } @Override @@ -375,19 +409,15 @@ public class DBTraceObjectMemoryRegion implements TraceObjectMemoryRegion, DBTra protected void updateViewsValueChanged(Lifespan lifespan, String key, Object oldValue, Object newValue) { DBTrace trace = object.getTrace(); - switch (key) { - case TargetMemoryRegion.RANGE_ATTRIBUTE_NAME: - // NB. old/newValue are null here. The CREATED event just has the new entry. - trace.updateViewsRefreshBlocks(); - return; - case TargetObject.DISPLAY_ATTRIBUTE_NAME: - trace.updateViewsChangeRegionBlockName(this); - return; - case TargetMemoryRegion.READABLE_ATTRIBUTE_NAME: - case TargetMemoryRegion.WRITABLE_ATTRIBUTE_NAME: - case TargetMemoryRegion.EXECUTABLE_ATTRIBUTE_NAME: - trace.updateViewsChangeRegionBlockFlags(this, lifespan); - return; + if (translator.keys.isRange(key)) { + // NB. old/newValue are null here. The CREATED event just has the new entry. + trace.updateViewsRefreshBlocks(); + } + else if (translator.keys.isDisplay(key)) { + trace.updateViewsChangeRegionBlockName(this); + } + else if (translator.keys.isFlag(key)) { + trace.updateViewsChangeRegionBlockFlags(this, lifespan); } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/module/DBTraceObjectModule.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/module/DBTraceObjectModule.java index ce7812657e..7c05ffdaa8 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/module/DBTraceObjectModule.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/module/DBTraceObjectModule.java @@ -15,11 +15,11 @@ */ package ghidra.trace.database.module; -import java.util.Collection; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; import ghidra.dbg.target.*; +import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.util.PathMatcher; import ghidra.dbg.util.PathPredicates.Align; import ghidra.dbg.util.PathUtils; @@ -40,8 +40,19 @@ import ghidra.util.exception.DuplicateNameException; public class DBTraceObjectModule implements TraceObjectModule, DBTraceObjectInterface { protected class ModuleChangeTranslator extends Translator { + private static final Map> KEYS_BY_SCHEMA = + new WeakHashMap<>(); + + private final Set keys; + protected ModuleChangeTranslator(DBTraceObject object, TraceModule iface) { super(TargetModule.RANGE_ATTRIBUTE_NAME, object, iface); + TargetObjectSchema schema = object.getTargetSchema(); + synchronized (KEYS_BY_SCHEMA) { + keys = KEYS_BY_SCHEMA.computeIfAbsent(schema, s -> Set.of( + s.checkAliasedAttribute(TargetModule.RANGE_ATTRIBUTE_NAME), + s.checkAliasedAttribute(TargetObject.DISPLAY_ATTRIBUTE_NAME))); + } } @Override @@ -61,8 +72,7 @@ public class DBTraceObjectModule implements TraceObjectModule, DBTraceObjectInte @Override protected boolean appliesToKey(String key) { - return TargetModule.RANGE_ATTRIBUTE_NAME.equals(key) || - TargetObject.DISPLAY_ATTRIBUTE_NAME.equals(key); + return keys.contains(key); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/module/DBTraceObjectSection.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/module/DBTraceObjectSection.java index 1c5de0039d..b5fd9e1dc7 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/module/DBTraceObjectSection.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/module/DBTraceObjectSection.java @@ -15,7 +15,10 @@ */ package ghidra.trace.database.module; +import java.util.*; + import ghidra.dbg.target.*; +import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.AddressRange; import ghidra.trace.database.target.DBTraceObject; @@ -34,8 +37,19 @@ import ghidra.util.LockHold; public class DBTraceObjectSection implements TraceObjectSection, DBTraceObjectInterface { protected class SectionTranslator extends Translator { + private static final Map> KEYS_BY_SCHEMA = + new WeakHashMap<>(); + + private final Set keys; + protected SectionTranslator(DBTraceObject object, TraceSection iface) { super(TargetSection.RANGE_ATTRIBUTE_NAME, object, iface); + TargetObjectSchema schema = object.getTargetSchema(); + synchronized (KEYS_BY_SCHEMA) { + keys = KEYS_BY_SCHEMA.computeIfAbsent(schema, s -> Set.of( + s.checkAliasedAttribute(TargetSection.RANGE_ATTRIBUTE_NAME), + s.checkAliasedAttribute(TargetObject.DISPLAY_ATTRIBUTE_NAME))); + } } @Override @@ -55,8 +69,7 @@ public class DBTraceObjectSection implements TraceObjectSection, DBTraceObjectIn @Override protected boolean appliesToKey(String key) { - return TargetSection.RANGE_ATTRIBUTE_NAME.equals(key) || - TargetObject.DISPLAY_ATTRIBUTE_NAME.equals(key); + return keys.contains(key); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/stack/DBTraceObjectStackFrame.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/stack/DBTraceObjectStackFrame.java index 0746fb4501..8a6ae0bca8 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/stack/DBTraceObjectStackFrame.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/stack/DBTraceObjectStackFrame.java @@ -15,9 +15,10 @@ */ package ghidra.trace.database.stack; -import java.util.List; +import java.util.*; -import ghidra.dbg.target.TargetStackFrame; +import ghidra.dbg.target.*; +import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.dbg.util.PathUtils; import ghidra.program.model.address.Address; import ghidra.program.model.listing.CodeUnit; @@ -37,13 +38,23 @@ import ghidra.trace.util.TraceChangeRecord; import ghidra.util.LockHold; public class DBTraceObjectStackFrame implements TraceObjectStackFrame, DBTraceObjectInterface { + private static final Map> KEYS_BY_SCHEMA = new WeakHashMap<>(); + private final DBTraceObject object; + private final Set keys; + // TODO: Memorizing life is not optimal. // GP-1887 means to expose multiple lifespans in, e.g., TraceThread private LifeSet life = new DefaultLifeSet(); public DBTraceObjectStackFrame(DBTraceObject object) { this.object = object; + + TargetObjectSchema schema = object.getTargetSchema(); + synchronized (KEYS_BY_SCHEMA) { + keys = KEYS_BY_SCHEMA.computeIfAbsent(schema, s -> Set.of( + schema.checkAliasedAttribute(TargetStackFrame.PC_ATTRIBUTE_NAME))); + } } @Override @@ -136,7 +147,7 @@ public class DBTraceObjectStackFrame implements TraceObjectStackFrame, DBTraceOb TraceObjectChangeType.VALUE_CREATED.cast(rec); TraceObjectValue affected = cast.getAffectedObject(); assert affected.getParent() == object; - if (!TargetStackFrame.PC_ATTRIBUTE_NAME.equals(affected.getEntryKey())) { + if (!keys.contains(affected.getEntryKey())) { return false; } if (object.getCanonicalParent(affected.getMaxSnap()) == null) { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java index 7bab3581a8..bb68c682ed 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObject.java @@ -137,6 +137,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { protected final DBTraceObjectManager manager; + private TargetObjectSchema targetSchema; private Map, TraceObjectInterface> ifaces; private final Map valueCache = new LinkedHashMap<>() { @@ -461,29 +462,31 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { @Override public Collection getValues(Lifespan span, String key) { try (LockHold hold = manager.trace.lockRead()) { - return doGetValues(span, key, true); + String k = getTargetSchema().checkAliasedAttribute(key); + return doGetValues(span, k, true); } } @Override public InternalTraceObjectValue getValue(long snap, String key) { try (LockHold hold = manager.trace.lockRead()) { - InternalTraceObjectValue cached = valueCache.get(key); + String k = getTargetSchema().checkAliasedAttribute(key); + InternalTraceObjectValue cached = valueCache.get(k); if (cached != null && !cached.isDeleted() && cached.getLifespan().contains(snap)) { return cached; } - Long nullSnap = nullCache.get(key); + Long nullSnap = nullCache.get(k); if (nullSnap != null && nullSnap.longValue() == snap) { return null; } InternalTraceObjectValue found = manager.valueMap - .reduce(TraceObjectValueQuery.values(this, key, key, Lifespan.at(snap))) + .reduce(TraceObjectValueQuery.values(this, k, k, Lifespan.at(snap))) .firstValue(); if (found == null) { - nullCache.put(key, snap); + nullCache.put(k, snap); } else { - valueCache.put(key, found); + valueCache.put(k, found); } return found; } @@ -493,7 +496,8 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { public Stream getOrderedValues(Lifespan span, String key, boolean forward) { try (LockHold hold = manager.trace.lockRead()) { - return doGetValues(span, key, forward).stream(); + String k = getTargetSchema().checkAliasedAttribute(key); + return doGetValues(span, k, forward).stream(); } } @@ -599,11 +603,12 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { if (isDeleted()) { throw new IllegalStateException("Cannot set value on deleted object."); } + String k = getTargetSchema().checkAliasedAttribute(key); if (resolution == ConflictResolution.DENY) { - doCheckConflicts(lifespan, key, value); + doCheckConflicts(lifespan, k, value); } else if (resolution == ConflictResolution.ADJUST) { - lifespan = doAdjust(lifespan, key, value); + lifespan = doAdjust(lifespan, k, value); } var setter = new ValueLifespanSetter(lifespan, value) { DBTraceObject canonicalLifeChanged = null; @@ -612,7 +617,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { protected Iterable getIntersecting(Long lower, Long upper) { return Collections.unmodifiableCollection( - doGetValues(Lifespan.span(lower, upper), key, true)); + doGetValues(Lifespan.span(lower, upper), k, true)); } @Override @@ -634,7 +639,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { @Override protected InternalTraceObjectValue create(Lifespan range, Object value) { - return doCreateValue(range, key, value); + return doCreateValue(range, k, value); } }; InternalTraceObjectValue result = setter.set(lifespan, value); @@ -673,7 +678,11 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject { @Override public TargetObjectSchema getTargetSchema() { - return manager.rootSchema.getSuccessorSchema(path.getKeyList()); + // NOTE: No need to synchronize. Schema is immutable. + if (targetSchema == null) { + targetSchema = manager.rootSchema.getSuccessorSchema(path.getKeyList()); + } + return targetSchema; } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectInterface.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectInterface.java index e4ada7ef1b..f91791c4f3 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectInterface.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectInterface.java @@ -15,6 +15,7 @@ */ package ghidra.trace.database.target; +import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressRange; import ghidra.trace.database.space.DBTraceSpaceKey.DefaultDBTraceSpaceKey; @@ -38,7 +39,13 @@ public interface DBTraceObjectInterface extends TraceObjectInterface, TraceUniqu private LifeSet life = new DefaultLifeSet(); public Translator(String spaceValueKey, DBTraceObject object, T iface) { - this.spaceValueKey = spaceValueKey; + if (spaceValueKey == null) { + this.spaceValueKey = null; + } + else { + TargetObjectSchema schema = object.getTargetSchema(); + this.spaceValueKey = schema.checkAliasedAttribute(spaceValueKey); + } this.object = object; this.iface = iface; } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectManager.java index 03d1bd275b..7f90d04b43 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/target/DBTraceObjectManager.java @@ -174,6 +174,8 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager return size() > OBJECTS_CONTAINING_CACHE_SIZE; } }; + protected final Map, Set> // + schemasByInterface = new HashMap<>(); public DBTraceObjectManager(DBHandle dbh, DBOpenMode openMode, ReadWriteLock lock, TaskMonitor monitor, Language baseLanguage, DBTrace trace) @@ -221,6 +223,9 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager valueTree.invalidateCache(); schemaStore.invalidateCache(); loadRootSchema(); + objectsContainingCache.clear(); + // Though rare, the root schema could change + schemasByInterface.clear(); } @Internal @@ -449,6 +454,8 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager objectStore.deleteAll(); schemaStore.deleteAll(); rootSchema = null; + objectsContainingCache.clear(); + schemasByInterface.clear(); } } @@ -515,13 +522,6 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager } } - protected Stream doParentsHaving( - Stream values, Class iface) { - return values.map(v -> v.getParent()) - .map(o -> o.queryInterface(iface)) - .filter(i -> i != null); - } - protected void invalidateObjectsContainingCache() { synchronized (objectsContainingCache) { objectsContainingCache.clear(); @@ -530,8 +530,8 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager protected Collection doGetObjectsContaining( ObjectsContainingKey key) { - return doParentsHaving(getValuesAt(key.snap, key.address, key.key).stream(), key.iface) - .collect(Collectors.toSet()); + return getObjectsIntersecting(Lifespan.at(key.snap), + new AddressRangeImpl(key.address, key.address), key.key, key.iface); } @SuppressWarnings("unchecked") @@ -555,11 +555,36 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager return col.iterator().next(); } + protected Set collectSchemasForInterface( + Class iface) { + if (rootSchema == null) { + return Set.of(); + } + Class targetIf = TraceObjectInterfaceUtils.toTargetIf(iface); + Set result = new HashSet<>(); + for (TargetObjectSchema schema : rootSchema.getContext().getAllSchemas()) { + if (schema.getInterfaces().contains(targetIf)) { + result.add(schema); + } + } + return Set.copyOf(result); + } + public Collection getObjectsIntersecting( Lifespan lifespan, AddressRange range, String key, Class iface) { try (LockHold hold = trace.lockRead()) { - return doParentsHaving(getValuesIntersecting(lifespan, range, key).stream(), iface) - .collect(Collectors.toSet()); + Set schemas; + synchronized (schemasByInterface) { + schemas = + schemasByInterface.computeIfAbsent(iface, this::collectSchemasForInterface); + } + Map> schemasByAliasTo = + schemas.stream().collect(Collectors.groupingBy(s -> s.checkAliasedAttribute(key))); + return schemasByAliasTo.entrySet().stream().flatMap(ent -> { + return getValuesIntersecting(lifespan, range, ent.getKey()).stream() + .map(v -> v.getParent()) + .filter(o -> ent.getValue().contains(o.getTargetSchema())); + }).map(o -> o.queryInterface(iface)).collect(Collectors.toSet()); } } @@ -573,7 +598,7 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager public AddressSetView getObjectsAddressSet(long snap, String key, Class ifaceCls, Predicate predicate) { return valueMap.getAddressSetView(Lifespan.at(snap), v -> { - if (!key.equals(v.getEntryKey())) { + if (!v.hasEntryKey(key)) { return false; } TraceObject parent = v.getParent(); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/thread/DBTraceObjectThread.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/thread/DBTraceObjectThread.java index 7831116524..61d392e7a0 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/thread/DBTraceObjectThread.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/thread/DBTraceObjectThread.java @@ -15,7 +15,10 @@ */ package ghidra.trace.database.thread; +import java.util.*; + import ghidra.dbg.target.TargetObject; +import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.trace.database.target.DBTraceObject; import ghidra.trace.database.target.DBTraceObjectInterface; import ghidra.trace.model.Lifespan; @@ -32,8 +35,19 @@ import ghidra.util.exception.DuplicateNameException; public class DBTraceObjectThread implements TraceObjectThread, DBTraceObjectInterface { protected class ThreadChangeTranslator extends Translator { + private static final Map> KEYS_BY_SCHEMA = + new WeakHashMap<>(); + + private final Set keys; + protected ThreadChangeTranslator(DBTraceObject object, TraceThread iface) { super(null, object, iface); + TargetObjectSchema schema = object.getTargetSchema(); + synchronized (KEYS_BY_SCHEMA) { + keys = KEYS_BY_SCHEMA.computeIfAbsent(schema, s -> Set.of( + s.checkAliasedAttribute(KEY_COMMENT), + s.checkAliasedAttribute(TargetObject.DISPLAY_ATTRIBUTE_NAME))); + } } @Override @@ -53,8 +67,7 @@ public class DBTraceObjectThread implements TraceObjectThread, DBTraceObjectInte @Override protected boolean appliesToKey(String key) { - return KEY_COMMENT.equals(key) || - TargetObject.DISPLAY_ATTRIBUTE_NAME.equals(key); + return keys.contains(key); } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java index fac5a58824..2210c998c6 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObject.java @@ -86,7 +86,7 @@ public interface TraceObject extends TraceUniqueObject { * contain the given lifespan. Only the canonical path is considered when looking for existing * ancestry. * - * @param the minimum lifespan of edges from the root to this object + * @param lifespan the minimum lifespan of edges from the root to this object * @param resolution the rule for handling duplicate keys when setting values. * @return the value path from root to the newly inserted object */ @@ -155,6 +155,9 @@ public interface TraceObject extends TraceUniqueObject { /** * Get all paths actually leading to this object, from the root, within the given span * + *

+ * Aliased keys are excluded. + * * @param span the span which every value entry on each path must intersect * @return the paths */ @@ -204,6 +207,9 @@ public interface TraceObject extends TraceUniqueObject { /** * Get all values intersecting the given span and whose child is this object * + *

+ * Aliased keys are excluded. + * * @param span the span * @return the parent values */ @@ -212,6 +218,10 @@ public interface TraceObject extends TraceUniqueObject { /** * Get all values (elements and attributes) of this object intersecting the given span * + *

+ * Aliased keys are excluded. + * + * @param span the span * @return the values */ Collection getValues(Lifespan span); @@ -219,6 +229,9 @@ public interface TraceObject extends TraceUniqueObject { /** * Get values with the given key intersecting the given span * + *

+ * If the key is an alias, the target key's values are retrieved instead. + * * @param span the span * @param key the key * @return the collection of values @@ -228,6 +241,9 @@ public interface TraceObject extends TraceUniqueObject { /** * Get values with the given key intersecting the given span ordered by time * + *

+ * If the key is an alias, the target key's values are retrieved instead. + * * @param span the span * @param key the key * @param forward true to order from least- to most-recent, false for most- to least-recent @@ -239,6 +255,7 @@ public interface TraceObject extends TraceUniqueObject { /** * Get all elements of this object intersecting the given span * + * @param span the span * @return the element values */ Collection getElements(Lifespan span); @@ -246,6 +263,10 @@ public interface TraceObject extends TraceUniqueObject { /** * Get all attributes of this object intersecting the given span * + *

+ * Aliased keys are excluded. + * + * @param span the span * @return the attribute values */ Collection getAttributes(Lifespan span); @@ -253,6 +274,9 @@ public interface TraceObject extends TraceUniqueObject { /** * Get the value for the given snap and key * + *

+ * If the key is an alias, the target key's value is retrieved instead. + * * @param snap the snap * @param key the key * @return the value entry @@ -302,6 +326,10 @@ public interface TraceObject extends TraceUniqueObject { * Stream all ancestor values of this object matching the given predicates, intersecting the * given span * + *

+ * Aliased keys are excluded. The predicates should be formulated to use the aliases' target + * attributes. + * * @param span a span which values along the path must intersect * @param rootPredicates the predicates for matching path keys, relative to the root * @return the stream of matching paths to values @@ -313,6 +341,10 @@ public interface TraceObject extends TraceUniqueObject { * Stream all ancestor values of this object matching the given predicates, intersecting the * given span * + *

+ * Aliased keys are excluded. The predicates should be formulated to use the aliases' target + * attributes. + * * @param span a span which values along the path must intersect * @param relativePredicates the predicates for matching path keys, relative to this object * @return the stream of matching paths to values @@ -324,6 +356,10 @@ public interface TraceObject extends TraceUniqueObject { * Stream all successor values of this object matching the given predicates, intersecting the * given span * + *

+ * Aliased keys are excluded. The predicates should be formulated to use the aliases' target + * attributes. + * * @param span a span which values along the path must intersect * @param relativePredicates the predicates for matching path keys, relative to this object * @return the stream of matching paths to values @@ -335,6 +371,10 @@ public interface TraceObject extends TraceUniqueObject { * Stream all successor values of this object at the given relative path, intersecting the given * span, ordered by time. * + *

+ * Aliased keys are excluded. The predicates should be formulated to use the aliases' target + * attributes. + * * @param span the span which values along the path must intersect * @param relativePath the path relative to this object * @param forward true to order from least- to most-recent, false for most- to least-recent @@ -348,9 +388,11 @@ public interface TraceObject extends TraceUniqueObject { * *

* If an object has a disjoint life, i.e., multiple canonical parents, then only the - * least-recent of those is traversed. + * least-recent of those is traversed. Aliased keys are excluded; those can't be canonical + * anyway. * - * @param relativePath the path relative to this object + * @param relativePredicates predicates on the relative path from this object to desired + * successors * @return the stream of value paths */ Stream getCanonicalSuccessors(PathPredicates relativePredicates); @@ -358,6 +400,9 @@ public interface TraceObject extends TraceUniqueObject { /** * Set a value for the given lifespan * + *

+ * If the key is an alias, the target key's value is set instead. + * * @param lifespan the lifespan of the value * @param key the key to set * @param value the new value @@ -374,7 +419,8 @@ public interface TraceObject extends TraceUniqueObject { *

* Setting a value of {@code null} effectively deletes the value for the given lifespan and * returns {@code null}. Values of the same key intersecting the given lifespan or either - * truncated or deleted. + * truncated or deleted. If the key is an alias, the target key's value is set instead. + * * * @param lifespan the lifespan of the value * @param key the key to set @@ -453,7 +499,7 @@ public interface TraceObject extends TraceUniqueObject { * Search for ancestors on the canonical path having the given target interface * *

- * The object may not yet be inserted at its canonical path + * The object may not yet be inserted at its canonical path. * * @param targetIf the interface class * @return the stream of objects @@ -465,7 +511,7 @@ public interface TraceObject extends TraceUniqueObject { * Search for ancestors on the canonical path providing the given interface * *

- * The object may not yet be inserted at its canonical path + * The object may not yet be inserted at its canonical path. * * @param the interface type * @param ifClass the interface class diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectValue.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectValue.java index e7e13ddb86..eb4d6dfcf6 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectValue.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/target/TraceObjectValue.java @@ -43,6 +43,20 @@ public interface TraceObjectValue { */ String getEntryKey(); + /** + * Check if the given key (or alias) matches this entry's key + * + * @param keyOrAlias the key or alias + * @return true if the key matches this entry's key, or it is an alias for it + */ + default boolean hasEntryKey(String keyOrAlias) { + TraceObject parent = getParent(); + if (parent == null) { + return getEntryKey().equals(keyOrAlias); + } + return getEntryKey().equals(parent.getTargetSchema().checkAliasedAttribute(keyOrAlias)); + } + /** * Get the "canonical path" of this value * @@ -50,7 +64,7 @@ public interface TraceObjectValue { * This is the parent's canonical path extended by this value's entry key. Note, in the case * this value has a child object, this is not necessarily its canonical path. * - * @return + * @return the canonical path */ TraceObjectKeyPath getCanonicalPath(); @@ -127,7 +141,7 @@ public interface TraceObjectValue { * uniquely determined at a given snap. Thus, when lifespans are being adjusted, such conflicts * must be resolved. * - * @param lifespan the new lifespan + * @param span the new lifespan * @param resolution specifies how to resolve duplicate keys with intersecting lifespans * @throws DuplicateKeyException if there are denied duplicate keys */ diff --git a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/target/DBTraceObjectManagerTest.java b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/target/DBTraceObjectManagerTest.java index c98568d2f1..26c55c8a96 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/target/DBTraceObjectManagerTest.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/test/java/ghidra/trace/database/target/DBTraceObjectManagerTest.java @@ -88,6 +88,7 @@ public class DBTraceObjectManagerTest extends AbstractGhidraHeadlessIntegrationT + """; @@ -1175,4 +1176,30 @@ public class DBTraceObjectManagerTest extends AbstractGhidraHeadlessIntegrationT b.trace.getObjectManager().getValuesIntersecting(Lifespan.ALL, b.range(0, -1)); } + + @Test + public void testAttributeAliasing() { + TraceObject regionText; + try (Transaction tx = b.startTransaction()) { + TraceObjectValue rootVal = + manager.createRootObject(ctx.getSchema(new SchemaName("Session"))); + root = rootVal.getChild(); + + regionText = + manager.createObject(TraceObjectKeyPath.parse("Targets[0].Memory[bin:.text]")); + regionText.insert(Lifespan.nowOn(0), ConflictResolution.DENY); + regionText.setAttribute(Lifespan.nowOn(0), "_range", b.range(0x00400000, 0x00401000)); + regionText.setAttribute(Lifespan.nowOn(0), "Range", b.range(0x00400000, 0x00402000)); + } + + assertEquals(ctx.getSchema(new SchemaName("Region")), regionText.getTargetSchema()); + assertEquals(Set.of( + regionText.getAttribute(0, "Range")), + Set.copyOf(regionText.getAttributes(Lifespan.ALL))); + assertEquals(b.range(0x00400000, 0x00402000), + regionText.getAttribute(0, "_range").getValue()); + assertEquals(b.range(0x00400000, 0x00402000), + regionText.getAttribute(0, "Range").getValue()); + assertEquals("Range", regionText.getAttribute(0, "_range").getEntryKey()); + } } diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerIntegrationTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerIntegrationTest.java index 773c4f3afd..b42ee57fdb 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerIntegrationTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/AbstractGhidraHeadedDebuggerIntegrationTest.java @@ -49,7 +49,8 @@ public class AbstractGhidraHeadedDebuggerIntegrationTest -