diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/RemoteMethod.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/RemoteMethod.java index 08d54c1dbd..ee194ce296 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/RemoteMethod.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/RemoteMethod.java @@ -106,23 +106,28 @@ public interface RemoteMethod { * primitive. We instead need {@link TraceObject}. I'd add the method to the schema, except that * trace stuff is not in its dependencies. * - * @param name the name of the parameter + * @param paramName the name of the parameter + * @param schName the name of the parameter's schema * @param sch the type of the parameter * @param arg the argument */ - static void checkType(String name, TargetObjectSchema sch, Object arg) { - if (sch.getType() != TargetObject.class) { - if (sch.getType().isInstance(arg)) { - return; + static void checkType(String paramName, SchemaName schName, TargetObjectSchema sch, + Object arg) { + // if sch is null, it was definitely an object-type schema without context + if (sch != null) { + if (sch.getType() != TargetObject.class) { + if (sch.getType().isInstance(arg)) { + return; + } } - } - else if (arg instanceof TraceObject obj) { - if (sch.equals(obj.getTargetSchema())) { - return; + else if (arg instanceof TraceObject obj) { + if (sch.isAssignableFrom(obj.getTargetSchema())) { + return; + } } } throw new IllegalArgumentException( - "For parameter %s: argument %s is not a %s".formatted(name, arg, sch)); + "For parameter %s: argument %s is not a %s".formatted(paramName, arg, schName)); } /** @@ -159,8 +164,9 @@ public interface RemoteMethod { "All TraceObject parameters must come from the same trace"); } } - TargetObjectSchema sch = ctx.getSchema(ent.getValue().type()); - checkType(ent.getKey(), sch, arg); + SchemaName schName = ent.getValue().type(); + TargetObjectSchema sch = ctx.getSchemaOrNull(schName); + checkType(ent.getKey(), schName, sch, arg); } for (Map.Entry ent : arguments.entrySet()) { if (!parameters().containsKey(ent.getKey())) { @@ -198,6 +204,7 @@ public interface RemoteMethod { * * @param arguments the keyword arguments to the remote method * @throws IllegalArgumentException if the arguments are not valid + * @return the returned value */ default Object invoke(Map arguments) { try { diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/RemoteMethodTest.java b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/RemoteMethodTest.java new file mode 100644 index 0000000000..79c12aebe8 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/test/java/ghidra/app/plugin/core/debug/service/tracermi/RemoteMethodTest.java @@ -0,0 +1,190 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.service.tracermi; + +import static ghidra.app.plugin.core.debug.gui.model.DebuggerModelProviderTest.CTX; + +import java.util.Map; + +import org.junit.Test; + +import db.Transaction; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; +import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiConnection.TestRemoteMethod; +import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiConnection.TestRemoteParameter; +import ghidra.dbg.target.schema.EnumerableTargetObjectSchema; +import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName; +import ghidra.debug.api.target.ActionName; +import ghidra.debug.api.tracermi.RemoteMethod; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.target.*; +import ghidra.trace.model.target.TraceObject.ConflictResolution; + +public class RemoteMethodTest extends AbstractGhidraHeadedDebuggerTest { + @Test + public void testRemoteMethodValidateObjectGivenObject() throws Throwable { + RemoteMethod method = new TestRemoteMethod("test", ActionName.name("test"), "Test", + "A test method", EnumerableTargetObjectSchema.VOID.getName(), + new TestRemoteParameter("obj", EnumerableTargetObjectSchema.OBJECT.getName(), true, + null, "Arg1", "An argument")); + + createTrace(); + + TraceObject root; + try (Transaction tx = tb.startTransaction()) { + TraceObjectValue rv = tb.trace.getObjectManager() + .createRootObject(CTX.getSchema(new SchemaName("Session"))); + root = rv.getChild(); + } + + method.validate(Map.of("obj", root)); + } + + @Test + public void testRemoteMethodValidateObjectGivenProcess() throws Throwable { + RemoteMethod method = new TestRemoteMethod("test", ActionName.name("test"), "Test", + "A test method", EnumerableTargetObjectSchema.VOID.getName(), + new TestRemoteParameter("obj", EnumerableTargetObjectSchema.OBJECT.getName(), true, + null, "Arg1", "An argument")); + + createTrace(); + + TraceObject process; + try (Transaction tx = tb.startTransaction()) { + tb.trace.getObjectManager().createRootObject(CTX.getSchema(new SchemaName("Session"))); + process = + tb.trace.getObjectManager().createObject(TraceObjectKeyPath.parse("Processes[0]")); + process.insert(Lifespan.nowOn(0), ConflictResolution.DENY); + } + + method.validate(Map.of("obj", process)); + } + + @Test(expected = IllegalArgumentException.class) + public void testRemoteMethodValidateObjectGivenInt() throws Throwable { + RemoteMethod method = new TestRemoteMethod("test", ActionName.name("test"), "Test", + "A test method", EnumerableTargetObjectSchema.VOID.getName(), + new TestRemoteParameter("obj", EnumerableTargetObjectSchema.OBJECT.getName(), true, + null, "Arg1", "An argument")); + + method.validate(Map.of("obj", 1)); + } + + @Test + public void testRemoteMethodValidateProcessGivenProcess() throws Throwable { + RemoteMethod method = new TestRemoteMethod("test", ActionName.name("test"), "Test", + "A test method", EnumerableTargetObjectSchema.VOID.getName(), + new TestRemoteParameter("proc", new SchemaName("Process"), true, + null, "Proc1", "A Process argument")); + + createTrace(); + + TraceObject process; + try (Transaction tx = tb.startTransaction()) { + tb.trace.getObjectManager().createRootObject(CTX.getSchema(new SchemaName("Session"))); + process = + tb.trace.getObjectManager().createObject(TraceObjectKeyPath.parse("Processes[0]")); + process.insert(Lifespan.nowOn(0), ConflictResolution.DENY); + } + + method.validate(Map.of("proc", process)); + } + + @Test(expected = IllegalArgumentException.class) + public void testRemoteMethodValidateProcessGivenInt() throws Throwable { + RemoteMethod method = new TestRemoteMethod("test", ActionName.name("test"), "Test", + "A test method", EnumerableTargetObjectSchema.VOID.getName(), + new TestRemoteParameter("proc", new SchemaName("Process"), true, + null, "Proc1", "A Process argument")); + + // Otherwise "Process" schema doesn't exist + createTrace(); + try (Transaction tx = tb.startTransaction()) { + tb.trace.getObjectManager().createRootObject(CTX.getSchema(new SchemaName("Session"))); + } + + method.validate(Map.of("proc", 1)); + } + + @Test + public void testRemoteMethodValidateAnyGivenInteger() throws Throwable { + RemoteMethod method = new TestRemoteMethod("test", ActionName.name("test"), "Test", + "A test method", EnumerableTargetObjectSchema.VOID.getName(), + new TestRemoteParameter("arg", EnumerableTargetObjectSchema.ANY.getName(), true, + null, "Arg1", "An argument")); + + method.validate(Map.of("arg", 1)); + } + + @Test + public void testRemoteMethodValidateAnyGivenObject() throws Throwable { + RemoteMethod method = new TestRemoteMethod("test", ActionName.name("test"), "Test", + "A test method", EnumerableTargetObjectSchema.VOID.getName(), + new TestRemoteParameter("arg", EnumerableTargetObjectSchema.ANY.getName(), true, + null, "Arg1", "An argument")); + + createTrace(); + + TraceObject root; + try (Transaction tx = tb.startTransaction()) { + TraceObjectValue rv = tb.trace.getObjectManager() + .createRootObject(CTX.getSchema(new SchemaName("Session"))); + root = rv.getChild(); + } + + method.validate(Map.of("arg", root)); + } + + @Test + public void testRemoteMethodValidateAnyGivenProcess() throws Throwable { + RemoteMethod method = new TestRemoteMethod("test", ActionName.name("test"), "Test", + "A test method", EnumerableTargetObjectSchema.VOID.getName(), + new TestRemoteParameter("arg", EnumerableTargetObjectSchema.ANY.getName(), true, + null, "Arg1", "An argument")); + + createTrace(); + + TraceObject process; + try (Transaction tx = tb.startTransaction()) { + tb.trace.getObjectManager().createRootObject(CTX.getSchema(new SchemaName("Session"))); + process = + tb.trace.getObjectManager().createObject(TraceObjectKeyPath.parse("Processes[0]")); + process.insert(Lifespan.nowOn(0), ConflictResolution.DENY); + } + + method.validate(Map.of("arg", process)); + } + + @Test + public void testRemoteMethodValidateIntegerGivenInteger() throws Throwable { + RemoteMethod method = new TestRemoteMethod("test", ActionName.name("test"), "Test", + "A test method", EnumerableTargetObjectSchema.VOID.getName(), + new TestRemoteParameter("arg", EnumerableTargetObjectSchema.INT.getName(), true, + null, "Arg1", "An argument")); + + method.validate(Map.of("arg", 1)); + } + + @Test(expected = IllegalArgumentException.class) + public void testRemoteMethodValidateIntegerGivenLong() throws Throwable { + RemoteMethod method = new TestRemoteMethod("test", ActionName.name("test"), "Test", + "A test method", EnumerableTargetObjectSchema.VOID.getName(), + new TestRemoteParameter("arg", EnumerableTargetObjectSchema.INT.getName(), true, + null, "Arg1", "An argument")); + + method.validate(Map.of("arg", 1L)); + } +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java index f2f3d695c0..4122d4952f 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProviderTest.java @@ -51,7 +51,7 @@ import ghidra.trace.model.thread.TraceThread; public class DebuggerModelProviderTest extends AbstractGhidraHeadedDebuggerTest { - protected static final SchemaContext CTX; + public static final SchemaContext CTX; static { try { 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 80bf2bc981..543684dc7b 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 @@ -46,6 +46,11 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema { public AttributeSchema getDefaultAttributeSchema() { return AttributeSchema.DEFAULT_ANY; } + + @Override + public boolean isAssignableFrom(TargetObjectSchema that) { + return true; + } }, /** * The least restrictive, but least informative object schema. @@ -63,6 +68,12 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema { public AttributeSchema getDefaultAttributeSchema() { return AttributeSchema.DEFAULT_ANY; } + + @Override + public boolean isAssignableFrom(TargetObjectSchema that) { + // That is has as schema implies it's a TargetObject + return true; + } }, TYPE(Class.class), /** 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 d5a9d0340c..e91e2dc13e 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 @@ -1233,4 +1233,20 @@ public interface TargetObjectSchema { } throw new IllegalArgumentException("No index between stack and frame"); } + + /** + * Check if this schema can accept a value of the given other schema + * + *

+ * This works analogously to {@link Class#isAssignableFrom(Class)}, except that schemas are + * quite a bit less flexible. Only {@link EnumerableTargetObjectSchema#ANY} and + * {@link EnumerableTargetObjectSchema#OBJECT} can accept anything other than exactly + * themselves. + * + * @param that + * @return true if an object of that schema can be assigned to this schema. + */ + default boolean isAssignableFrom(TargetObjectSchema that) { + return this.equals(that); + } }