From 4042cb8b938acb3395ce4f63a87b111b91a07ecf Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 9 Nov 2024 16:57:06 -0500 Subject: [PATCH 1/8] Change how logging UI prunes internal message content model data --- .../coley/recaf/ui/pane/LoggingPane.java | 81 +++++++++---------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/LoggingPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/LoggingPane.java index 97febf54b..9985ba911 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/LoggingPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/LoggingPane.java @@ -6,6 +6,7 @@ import jakarta.inject.Inject; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.control.Tooltip; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; @@ -16,6 +17,7 @@ import javafx.util.Duration; import org.fxmisc.richtext.CodeArea; import org.slf4j.event.Level; +import software.coley.recaf.analytics.logging.DebuggingLogger; import software.coley.recaf.analytics.logging.LogConsumer; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.services.file.RecafDirectoriesConfig; @@ -27,10 +29,14 @@ import software.coley.recaf.util.StringUtil; import software.coley.recaf.util.threading.ThreadPoolFactory; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.Random; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; /** * Pane for displaying logger calls. @@ -39,7 +45,8 @@ */ @Dependent public class LoggingPane extends BorderPane implements LogConsumer { - private final List infos = new ArrayList<>(); + private final List infos = Collections.synchronizedList(new ArrayList<>()); + private final Queue messageQueue = new ArrayDeque<>(); private final Editor editor = new Editor(); private final CodeArea codeArea = editor.getCodeArea(); @@ -52,7 +59,7 @@ public LoggingPane(@Nonnull RecafDirectoriesConfig config, @Nonnull SearchBar se setCenter(editor); // Initial line - infos.add(new LogCallInfo("Initial", Level.TRACE, "", null)); + infos.add(new LogCallInfo(Level.TRACE, null)); codeArea.appendText("Current log will write to: " + StringUtil.pathToAbsoluteString(config.getCurrentLogPath())); // We want to reduce the calls to the FX thread, so we will chunk log-appends into groups @@ -60,14 +67,18 @@ public LoggingPane(@Nonnull RecafDirectoriesConfig config, @Nonnull SearchBar se ThreadPoolFactory.newScheduledThreadPool("logging-pane") .scheduleAtFixedRate(() -> { try { - int skip = codeArea.getParagraphs().size(); - int size = infos.size(); - if (size > skip) { - String collectedMessageLines = infos.stream().skip(skip) - .map(LogCallInfo::getAndPruneContent) - .collect(Collectors.joining("\n")); + StringBuilder messageBuilder = new StringBuilder(); + synchronized (messageQueue) { + if (messageQueue.isEmpty()) + return; + String message; + while ((message = messageQueue.poll()) != null) + messageBuilder.append(message).append('\n'); + } + if (messageBuilder.length() > 1) { + String collectedMessage = messageBuilder.substring(0, messageBuilder.length() - 1); FxThreadUtil.run(() -> { - codeArea.appendText("\n" + collectedMessageLines); + codeArea.appendText("\n" + collectedMessage); codeArea.showParagraphAtBottom(codeArea.getParagraphs().size() - 1); }); } @@ -80,48 +91,29 @@ public LoggingPane(@Nonnull RecafDirectoriesConfig config, @Nonnull SearchBar se } @Override - public void accept(@Nonnull String loggerName, @Nonnull Level level, String messageContent) { - infos.add(new LogCallInfo(loggerName, level, messageContent, null)); + public void accept(@Nonnull String loggerName, @Nonnull Level level, @Nullable String messageContent) { + accept(loggerName, level, messageContent, null); } @Override - public void accept(@Nonnull String loggerName, @Nonnull Level level, String messageContent, Throwable throwable) { - infos.add(new LogCallInfo(loggerName, level, messageContent, throwable)); - } - - private static class LogCallInfo { - private final String loggerName; - private final Level level; - private final Throwable throwable; - private String messageContent; - - LogCallInfo(@Nonnull String loggerName, - @Nonnull Level level, - @Nonnull String messageContent, - @Nullable Throwable throwable) { - this.loggerName = loggerName; - this.level = level; - this.messageContent = messageContent; - this.throwable = throwable; + public void accept(@Nonnull String loggerName, @Nonnull Level level, @Nullable String messageContent, @Nullable Throwable throwable) { + if (messageContent == null) { + if (throwable == null) + return; + messageContent = Objects.requireNonNullElse(throwable.getMessage(), throwable.getClass().getSimpleName()); } - - /** - * Gets the message content once, then clears it, so we don't hold a constant reference to it. - * - * @return Message content of log call. - */ - @Nonnull - public String getAndPruneContent() { - String content = messageContent; - if (content == null) - throw new PruneError(); - messageContent = null; - return content; + infos.add(new LogCallInfo(level, throwable)); + synchronized (messageQueue) { + messageQueue.add(messageContent); } } + private record LogCallInfo( + @Nonnull Level level, + @Nullable Throwable throwable) {} + private class LoggingLineFactory implements LineGraphicFactory { - private static final Insets PADDING = new Insets(0, 5, 0, 0); + private static final Insets PADDING = new Insets(0, 10, 0, 0); private static final double SIZE = 4; private static final double[] TRIANGLE = { SIZE, 0, // Size used for circles is radius, so for triangles we want to double positions based on it. @@ -161,6 +153,7 @@ public void apply(@Nonnull LineContainer container, int paragraph) { HBox wrapper = new HBox(shape); wrapper.setAlignment(Pos.CENTER); wrapper.setPadding(PADDING); + wrapper.setCursor(Cursor.HAND); if (info.throwable != null) { Tooltip tooltip = new Tooltip(StringUtil.traceToString(info.throwable)); tooltip.setShowDelay(Duration.ZERO); From 950aa7f2f150991eeacc243b61f0c277ea28486f Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 9 Nov 2024 16:57:52 -0500 Subject: [PATCH 2/8] Make stub JVM class yield current runtime version since using 0 is an invalid class version --- .../src/main/java/software/coley/recaf/info/StubClassInfo.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/recaf-core/src/main/java/software/coley/recaf/info/StubClassInfo.java b/recaf-core/src/main/java/software/coley/recaf/info/StubClassInfo.java index dae567dc4..1662ca80c 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/StubClassInfo.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/StubClassInfo.java @@ -8,6 +8,7 @@ import software.coley.recaf.info.member.FieldMember; import software.coley.recaf.info.member.MethodMember; import software.coley.recaf.info.properties.Property; +import software.coley.recaf.util.JavaVersion; import java.util.Collections; import java.util.List; @@ -223,7 +224,7 @@ public JvmClassInfo asJvmClass() { @Override public int getVersion() { - return 0; + return JavaVersion.VERSION_OFFSET + JavaVersion.get(); } @Nonnull From 7136b045b25e206c0313e51fe34f37f99b9187b3 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 9 Nov 2024 18:46:16 -0500 Subject: [PATCH 3/8] Allow inspection of transforms before applying them to the workspace --- .../transform/JvmTransformerContext.java | 57 +++++++++++----- .../services/transform/TransformResult.java | 34 ++++++++++ .../transform/TransformationApplier.java | 68 ++++++++++++++++--- .../transform/TransformationApplierTest.java | 12 ++-- 4 files changed, 138 insertions(+), 33 deletions(-) create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/transform/TransformResult.java diff --git a/recaf-core/src/main/java/software/coley/recaf/services/transform/JvmTransformerContext.java b/recaf-core/src/main/java/software/coley/recaf/services/transform/JvmTransformerContext.java index 3f08ec31a..5f1065621 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/transform/JvmTransformerContext.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/transform/JvmTransformerContext.java @@ -5,13 +5,19 @@ import org.objectweb.asm.ClassWriter; import org.objectweb.asm.tree.ClassNode; import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.PathNodes; +import software.coley.recaf.path.ResourcePathNode; import software.coley.recaf.services.inheritance.InheritanceGraph; import software.coley.recaf.util.visitors.WorkspaceClassWriter; +import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; +import software.coley.recaf.workspace.model.resource.WorkspaceResource; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -24,34 +30,49 @@ public class JvmTransformerContext { private final Map, JvmClassTransformer> transformerMap; private final Map classData = new ConcurrentHashMap<>(); + private final Workspace workspace; + private final WorkspaceResource resource; /** * Constructs a new context from an array of transformers. * + * @param workspace + * Workspace containing the classes to transform. + * @param resource + * Resource in the workspace containing classes to transform. Should always be the {@link Workspace#getPrimaryResource()}. * @param transformers * Transformers to associate with this context. */ - public JvmTransformerContext(@Nonnull JvmClassTransformer... transformers) { - this.transformerMap = buildMap(transformers); + public JvmTransformerContext(@Nonnull Workspace workspace, @Nonnull WorkspaceResource resource, @Nonnull JvmClassTransformer... transformers) { + this(workspace, resource, Arrays.asList(transformers)); } /** * Constructs a new context from a collection of transformers. * + * @param workspace + * Workspace containing the classes to transform. + * @param resource + * Resource in the workspace containing classes to transform. Should always be the {@link Workspace#getPrimaryResource()}. * @param transformers * Transformers to associate with this context. */ - public JvmTransformerContext(@Nonnull Collection transformers) { + public JvmTransformerContext(@Nonnull Workspace workspace, @Nonnull WorkspaceResource resource, @Nonnull Collection transformers) { this.transformerMap = buildMap(transformers); + this.workspace = workspace; + this.resource = resource; } /** - * Apply any of the recorded changes within this context to the associated workspace. + * Builds the map of initial transformed class paths to their final transformed states. * * @param graph * Inheritance graph tied to the workspace the transformed classes belong to. */ - protected void applyChanges(@Nonnull InheritanceGraph graph) { + @Nonnull + protected Map buildChangeMap(@Nonnull InheritanceGraph graph) { + ResourcePathNode resourcePath = PathNodes.resourcePath(workspace, resource); + Map map = new HashMap<>(); for (JvmClassData data : classData.values()) { if (data.isDirty()) { if (data.node != null) { @@ -60,21 +81,30 @@ protected void applyChanges(@Nonnull InheritanceGraph graph) { data.node.accept(writer); byte[] modifiedBytes = writer.toByteArray(); - // Update workspace + // Update output map JvmClassInfo modifiedClass = data.initialClass.toJvmClassBuilder() .adaptFrom(modifiedBytes) .build(); - data.bundle.put(modifiedClass); + ClassPathNode classPath = resourcePath.child(data.bundle) + .child(modifiedClass.getPackageName()) + .child(modifiedClass); + map.put(classPath, modifiedClass); } else { - // Update workspace if the bytecode is not the same as the initial state + // Update output map if the bytecode is not the same as the initial state byte[] bytecode = data.getBytecode(); - if (!Arrays.equals(bytecode, data.initialClass.getBytecode())) - data.bundle.put(data.initialClass.toJvmClassBuilder() + if (!Arrays.equals(bytecode, data.initialClass.getBytecode())) { + JvmClassInfo modifiedClass = data.initialClass.toJvmClassBuilder() .adaptFrom(bytecode) - .build()); + .build(); + ClassPathNode classPath = resourcePath.child(data.bundle) + .child(modifiedClass.getPackageName()) + .child(modifiedClass); + map.put(classPath, modifiedClass); + } } } } + return map; } /** @@ -191,11 +221,6 @@ private JvmClassData getJvmClassData(@Nonnull JvmClassBundle bundle, @Nonnull Jv return classData.computeIfAbsent(info.getName(), ignored -> new JvmClassData(bundle, info)); } - @Nonnull - private static Map, JvmClassTransformer> buildMap(@Nonnull JvmClassTransformer[] transformers) { - return buildMap(Arrays.asList(transformers)); - } - @Nonnull private static Map, JvmClassTransformer> buildMap(@Nonnull Collection transformers) { Map, JvmClassTransformer> map = new IdentityHashMap<>(); diff --git a/recaf-core/src/main/java/software/coley/recaf/services/transform/TransformResult.java b/recaf-core/src/main/java/software/coley/recaf/services/transform/TransformResult.java new file mode 100644 index 000000000..17a187361 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/transform/TransformResult.java @@ -0,0 +1,34 @@ +package software.coley.recaf.services.transform; + +import jakarta.annotation.Nonnull; +import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.workspace.model.Workspace; + +import java.util.Map; + +/** + * Intermediate holder of transformations of workspace classes. You can inspect the state of the transformed classes + * before you apply the changes to the associated {@link Workspace}. + * + * @author Matt Coley + */ +public interface TransformResult { + /** + * Puts the transformed classes into the associated workspace. + */ + void apply(); + + /** + * @return Map of classes, to their maps of transformer-associated exceptions. + * Empty if transformation was a complete success (no failures). + */ + @Nonnull + Map, Throwable>> getJvmTransformerFailures(); + + /** + * @return Map of class paths to the original classes, to the resulting transformed class models. + */ + @Nonnull + Map getJvmTransformedClasses(); +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/transform/TransformationApplier.java b/recaf-core/src/main/java/software/coley/recaf/services/transform/TransformationApplier.java index 38da53d54..e0c61e846 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/transform/TransformationApplier.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/transform/TransformationApplier.java @@ -5,16 +5,25 @@ import jakarta.inject.Inject; import org.slf4j.Logger; import software.coley.collections.Sets; +import software.coley.collections.Unchecked; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.cdi.WorkspaceScoped; +import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.path.BundlePathNode; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.PathNodes; +import software.coley.recaf.path.ResourcePathNode; import software.coley.recaf.services.Service; import software.coley.recaf.services.inheritance.InheritanceGraph; import software.coley.recaf.workspace.model.Workspace; +import software.coley.recaf.workspace.model.bundle.JvmClassBundle; import software.coley.recaf.workspace.model.resource.WorkspaceResource; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -40,40 +49,52 @@ public TransformationApplier(@Nonnull TransformationManager manager, @Nonnull In } /** - * @param transformerClasses - * JVM class transformers to run. * @param workspace * Workspace to transform. + * @param transformerClasses + * JVM class transformers to run. + * + * @return Result container with details about the transformation, including any failures, the transformed classes, + * and the option to apply the transformations to the workspace. * * @throws TransformationException * When transformation cannot be run for any reason. */ - public void transformJvm(@Nonnull List> transformerClasses, - @Nonnull Workspace workspace) throws TransformationException { - transformJvm(transformerClasses, workspace, null); + @Nonnull + public TransformResult transformJvm(@Nonnull Workspace workspace, @Nonnull List> transformerClasses) throws TransformationException { + return transformJvm(workspace, transformerClasses, null); } /** - * @param transformerClasses - * JVM class transformers to run. * @param workspace * Workspace to transform. + * @param transformerClasses + * JVM class transformers to run. * @param predicate * Filter to control which JVM classes are transformed. * Can be {@code null} to transform all JVM classes. * + * @return Result container with details about the transformation, including any failures, the transformed classes, + * and the option to apply the transformations to the workspace. + * * @throws TransformationException * When transformation cannot be run for any reason. */ - public void transformJvm(@Nonnull List> transformerClasses, - @Nonnull Workspace workspace, @Nullable JvmClassTransformerPredicate predicate) throws TransformationException { + @Nonnull + public TransformResult transformJvm(@Nonnull Workspace workspace, @Nonnull List> transformerClasses, + @Nullable JvmClassTransformerPredicate predicate) throws TransformationException { // Build transformer visitation order TransformerQueue queue = buildQueue(transformerClasses); + // Map to hold transformation errors for each class:transformer + Map, Throwable>> transformJvmFailures = new HashMap<>(); + // Build the transformer context and apply all transformations in order - JvmTransformerContext context = new JvmTransformerContext(queue.transformers); WorkspaceResource resource = workspace.getPrimaryResource(); + ResourcePathNode resourcePath = PathNodes.resourcePath(workspace, resource); + JvmTransformerContext context = new JvmTransformerContext(workspace, resource, queue.transformers); resource.jvmClassBundleStreamRecursive().forEach(bundle -> { + BundlePathNode bundlePathNode = resourcePath.child(bundle); for (JvmClassTransformer transformer : queue.transformers) { bundle.forEach(cls -> { // Skip if the class does not pass the predicate @@ -84,13 +105,38 @@ public void transformJvm(@Nonnull List> tra transformer.transform(context, workspace, resource, bundle, cls); } catch (Throwable t) { logger.error("Transformer '{}' failed on class '{}'", transformer.name(), cls.getName(), t); + ClassPathNode path = bundlePathNode.child(cls.getPackageName()).child(cls); + var transformerToThrowable = transformJvmFailures.computeIfAbsent(path, p -> new HashMap<>()); + transformerToThrowable.put(transformer.getClass(), t); } }); } }); // Update the workspace contents with the transformation results - context.applyChanges(graph); + Map transformedJvmClasses = context.buildChangeMap(graph); + return new TransformResult() { + @Nonnull + @Override + public Map, Throwable>> getJvmTransformerFailures() { + return transformJvmFailures; + } + + @Nonnull + @Override + public Map getJvmTransformedClasses() { + return transformedJvmClasses; + } + + @Override + public void apply() { + Unchecked.checkedForEach(transformedJvmClasses, (path, cls) -> { + JvmClassBundle bundle = path.getValueOfType(JvmClassBundle.class); + if (bundle != null) + bundle.put(cls); + }, (path, cls, t) -> logger.error("Exception thrown handling transform application", t)); + } + }; // TODO: If we want a transformer which generates and applies mappings, we need a way to facilitate that // - Letting a transformer control mapping applier is bad because that can break the tracking state of class models diff --git a/recaf-core/src/test/java/software/coley/recaf/services/transform/TransformationApplierTest.java b/recaf-core/src/test/java/software/coley/recaf/services/transform/TransformationApplierTest.java index 6c3cdbd13..970efe862 100644 --- a/recaf-core/src/test/java/software/coley/recaf/services/transform/TransformationApplierTest.java +++ b/recaf-core/src/test/java/software/coley/recaf/services/transform/TransformationApplierTest.java @@ -61,7 +61,7 @@ void independentAB() { // If we transform with "B" we should observe that only "B" is called on sine the two hold no relation TransformationManager manager = new TransformationManager(map); TransformationApplier applier = new TransformationApplier(manager, graph, config); - assertDoesNotThrow(() -> applier.transformJvm(Collections.singletonList(JvmTransformerB.class), workspace)); + assertDoesNotThrow(() -> applier.transformJvm(workspace, Collections.singletonList(JvmTransformerB.class))); // "A" not used verify(transformerA, never()).transform(any(), any(), any(), any(), any()); @@ -85,7 +85,7 @@ void dependentAB() { // If we transform with "B" we should observe that both "B" and "A" were called on. TransformationManager manager = new TransformationManager(map); TransformationApplier applier = new TransformationApplier(manager, graph, config); - assertDoesNotThrow(() -> applier.transformJvm(Collections.singletonList(JvmTransformerDependingOnA.class), workspace)); + assertDoesNotThrow(() -> applier.transformJvm(workspace, Collections.singletonList(JvmTransformerDependingOnA.class))); verify(transformerA, times(1)).transform(any(), same(workspace), any(), any(), any()); verify(transformerB, times(1)).transform(any(), same(workspace), any(), any(), any()); } @@ -105,8 +105,8 @@ void cycleAB() { // If we transform with "A" or "B" we should observe an exception due to the detected cycle TransformationManager manager = new TransformationManager(map); TransformationApplier applier = new TransformationApplier(manager, graph, config); - assertThrows(TransformationException.class, () -> applier.transformJvm(Collections.singletonList(JvmCycleA.class), workspace)); - assertThrows(TransformationException.class, () -> applier.transformJvm(Collections.singletonList(JvmCycleB.class), workspace)); + assertThrows(TransformationException.class, () -> applier.transformJvm(workspace, Collections.singletonList(JvmCycleA.class))); + assertThrows(TransformationException.class, () -> applier.transformJvm(workspace, Collections.singletonList(JvmCycleB.class))); verify(transformerA, never()).transform(any(), same(workspace), any(), any(), any()); verify(transformerB, never()).transform(any(), same(workspace), any(), any(), any()); } @@ -123,7 +123,7 @@ void cycleSingle() { // If we transform with the single transformer we should observe an exception due to the detected cycle TransformationManager manager = new TransformationManager(map); TransformationApplier applier = new TransformationApplier(manager, graph, config); - assertThrows(TransformationException.class, () -> applier.transformJvm(Collections.singletonList(JvmCycleSingle.class), workspace)); + assertThrows(TransformationException.class, () -> applier.transformJvm(workspace, Collections.singletonList(JvmCycleSingle.class))); verify(transformer, never()).transform(any(), same(workspace), any(), any(), any()); } @@ -132,7 +132,7 @@ void missingRegistration() { // If we transform with a transformer that is not registered in the manager, the transform should fail TransformationManager manager = new TransformationManager(Collections.emptyMap()); TransformationApplier applier = new TransformationApplier(manager, graph, config); - assertThrows(TransformationException.class, () -> applier.transformJvm(Collections.singletonList(JvmCycleSingle.class), workspace)); + assertThrows(TransformationException.class, () -> applier.transformJvm(workspace, Collections.singletonList(JvmCycleSingle.class))); } static class JvmTransformerA implements JvmClassTransformer { From e829d2995e09dd64a4580707e4b5a6c256950f2a Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 9 Nov 2024 18:47:59 -0500 Subject: [PATCH 4/8] Create value tracking ASM analyzer/interpreter implementation --- .../java/software/coley/recaf/util/Types.java | 9 + .../coley/recaf/util/analysis/Nullness.java | 10 + .../coley/recaf/util/analysis/ReAnalyzer.java | 20 + .../recaf/util/analysis/ReInterpreter.java | 606 ++++++++++++++++++ .../recaf/util/analysis/value/ArrayValue.java | 117 ++++ .../util/analysis/value/DoubleValue.java | 158 +++++ .../recaf/util/analysis/value/FloatValue.java | 165 +++++ .../recaf/util/analysis/value/IntValue.java | 217 +++++++ .../recaf/util/analysis/value/LongValue.java | 221 +++++++ .../util/analysis/value/ObjectValue.java | 95 +++ .../recaf/util/analysis/value/ReValue.java | 24 + .../util/analysis/value/StringValue.java | 26 + .../analysis/value/UninitializedValue.java | 30 + .../analysis/value/impl/ArrayValueImpl.java | 75 +++ .../analysis/value/impl/DoubleValueImpl.java | 50 ++ .../analysis/value/impl/FloatValueImpl.java | 54 ++ .../analysis/value/impl/IntValueImpl.java | 50 ++ .../analysis/value/impl/LongValueImpl.java | 50 ++ .../analysis/value/impl/ObjectValueImpl.java | 60 ++ .../analysis/value/impl/StringValueImpl.java | 40 ++ .../value/impl/UninitializedValueImpl.java | 19 + 21 files changed, 2096 insertions(+) create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/Nullness.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/ReAnalyzer.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/ReInterpreter.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/ArrayValue.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/DoubleValue.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/FloatValue.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/IntValue.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/LongValue.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/ObjectValue.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/ReValue.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/StringValue.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/UninitializedValue.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/ArrayValueImpl.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/DoubleValueImpl.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/FloatValueImpl.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/IntValueImpl.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/LongValueImpl.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/ObjectValueImpl.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/StringValueImpl.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/UninitializedValueImpl.java diff --git a/recaf-core/src/main/java/software/coley/recaf/util/Types.java b/recaf-core/src/main/java/software/coley/recaf/util/Types.java index 8c72cd440..8f70ced19 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/Types.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/Types.java @@ -19,7 +19,16 @@ */ public class Types { public static final Type OBJECT_TYPE = Type.getObjectType("java/lang/Object"); + public static final Type CLASS_TYPE = Type.getObjectType("java/lang/Class"); public static final Type STRING_TYPE = Type.getObjectType("java/lang/String"); + public static final Type ARRAY_1D_BOOLEAN = Type.getObjectType("[Z"); + public static final Type ARRAY_1D_CHAR = Type.getObjectType("[C"); + public static final Type ARRAY_1D_BYTE = Type.getObjectType("[B"); + public static final Type ARRAY_1D_SHORT = Type.getObjectType("[S"); + public static final Type ARRAY_1D_INT = Type.getObjectType("[I"); + public static final Type ARRAY_1D_FLOAT = Type.getObjectType("[F"); + public static final Type ARRAY_1D_DOUBLE = Type.getObjectType("[D"); + public static final Type ARRAY_1D_LONG = Type.getObjectType("[J"); public static final Type[] PRIMITIVES = new Type[]{ Type.VOID_TYPE, Type.BOOLEAN_TYPE, diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/Nullness.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/Nullness.java new file mode 100644 index 000000000..55c7190d3 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/Nullness.java @@ -0,0 +1,10 @@ +package software.coley.recaf.util.analysis; + +/** + * Nullability state. + * + * @author Matt Coley + */ +public enum Nullness { + NULL, NOT_NULL, UNKNOWN +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/ReAnalyzer.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/ReAnalyzer.java new file mode 100644 index 000000000..2fe30d5dd --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/ReAnalyzer.java @@ -0,0 +1,20 @@ +package software.coley.recaf.util.analysis; + +import jakarta.annotation.Nonnull; +import org.objectweb.asm.tree.analysis.Analyzer; +import software.coley.recaf.util.analysis.value.ReValue; + +/** + * Analyzer that takes in an interpreter for {@link ReValue enhanced value types}. + * + * @author Matt Coley + */ +public class ReAnalyzer extends Analyzer { + /** + * @param interpreter + * Enhanced interpreter. + */ + public ReAnalyzer(@Nonnull ReInterpreter interpreter) { + super(interpreter); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/ReInterpreter.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/ReInterpreter.java new file mode 100644 index 000000000..403608dfa --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/ReInterpreter.java @@ -0,0 +1,606 @@ +package software.coley.recaf.util.analysis; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.objectweb.asm.ConstantDynamic; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.IincInsnNode; +import org.objectweb.asm.tree.IntInsnNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MultiANewArrayInsnNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.Interpreter; +import org.objectweb.asm.tree.analysis.SimpleVerifier; +import software.coley.recaf.RecafConstants; +import software.coley.recaf.services.inheritance.InheritanceGraph; +import software.coley.recaf.services.inheritance.InheritanceVertex; +import software.coley.recaf.util.Types; +import software.coley.recaf.util.analysis.value.ArrayValue; +import software.coley.recaf.util.analysis.value.DoubleValue; +import software.coley.recaf.util.analysis.value.FloatValue; +import software.coley.recaf.util.analysis.value.IntValue; +import software.coley.recaf.util.analysis.value.LongValue; +import software.coley.recaf.util.analysis.value.ObjectValue; +import software.coley.recaf.util.analysis.value.ReValue; +import software.coley.recaf.util.analysis.value.UninitializedValue; +import software.coley.recaf.util.analysis.value.impl.ArrayValueImpl; +import software.coley.recaf.util.analysis.value.impl.ObjectValueImpl; + +import java.util.List; +import java.util.OptionalInt; + +/** + * Interpreter implementation for enhanced value tracking. + * + * @author Matt Coley + * @see ReValue Base enhanced value type. + */ +public class ReInterpreter extends Interpreter implements Opcodes { + private final InheritanceGraph graph; + + public ReInterpreter(@Nonnull InheritanceGraph graph) { + super(RecafConstants.getAsmVersion()); + this.graph = graph; + } + + @Nonnull + @SuppressWarnings("DataFlowIssue") // Won't happen because we use arrays + private ReValue newArrayValue(@Nonnull Type type, int dimensions) { + if (dimensions == 0) + return newValue(type); + String descriptor = "[".repeat(Math.max(0, dimensions)) + type.getDescriptor(); + return newValue(Type.getType(descriptor), Nullness.NOT_NULL); + } + + @Nullable + public ReValue newValue(@Nullable Type type, @Nonnull Nullness nullness) { + if (type == null) + return UninitializedValue.UNINITIALIZED_VALUE; + return switch (type.getSort()) { + case Type.VOID -> null; + case Type.BOOLEAN, Type.CHAR, Type.BYTE, Type.SHORT, Type.INT -> IntValue.UNKNOWN; + case Type.FLOAT -> FloatValue.UNKNOWN; + case Type.LONG -> LongValue.UNKNOWN; + case Type.DOUBLE -> DoubleValue.UNKNOWN; + case Type.ARRAY -> new ArrayValueImpl(type, nullness); + case Type.OBJECT -> { + if (Types.OBJECT_TYPE.equals(type)) yield ObjectValue.object(nullness); + else if (Types.STRING_TYPE.equals(type)) yield ObjectValue.string(nullness); + yield new ObjectValueImpl(type, nullness); + } + default -> throw new IllegalArgumentException("Invalid type for new value: " + type); + }; + } + + @Override + public ReValue newValue(@Nullable Type type) { + return newValue(type, Nullness.UNKNOWN); + } + + @Override + public ReValue newOperation(@Nonnull AbstractInsnNode insn) throws AnalyzerException { + switch (insn.getOpcode()) { + case ACONST_NULL: + return ObjectValue.VAL_OBJECT_NULL; + case ICONST_M1: + return IntValue.VAL_M1; + case ICONST_0: + return IntValue.VAL_0; + case ICONST_1: + return IntValue.VAL_1; + case ICONST_2: + return IntValue.VAL_2; + case ICONST_3: + return IntValue.VAL_3; + case ICONST_4: + return IntValue.VAL_4; + case ICONST_5: + return IntValue.VAL_5; + case LCONST_0: + return LongValue.VAL_0; + case LCONST_1: + return LongValue.VAL_1; + case FCONST_0: + return FloatValue.VAL_0; + case FCONST_1: + return FloatValue.VAL_1; + case FCONST_2: + return FloatValue.VAL_2; + case DCONST_0: + return DoubleValue.VAL_0; + case DCONST_1: + return DoubleValue.VAL_1; + case BIPUSH: + case SIPUSH: + IntInsnNode intInsn = (IntInsnNode) insn; + return IntValue.of(intInsn.operand); + case LDC: + Object value = ((LdcInsnNode) insn).cst; + switch (value) { + case Integer i -> { + return IntValue.of(i); + } + case Float f -> { + return FloatValue.of(f); + } + case Long l -> { + return LongValue.of(l); + } + case Double d -> { + return DoubleValue.of(d); + } + case String s -> { + return ObjectValue.string(s); + } + case Type type -> { + int sort = type.getSort(); + if (sort == Type.OBJECT || sort == Type.ARRAY) { + return ObjectValue.VAL_CLASS; + } else if (sort == Type.METHOD) { + return ObjectValue.VAL_METHOD_TYPE; + } else { + throw new AnalyzerException(insn, "Illegal LDC value " + value); + } + } + case Handle handle -> { + return ObjectValue.VAL_METHOD_HANDLE; + } + case ConstantDynamic constantDynamic -> { + Type dynamicType = Type.getType(constantDynamic.getDescriptor()); + return newValue(dynamicType, Nullness.NOT_NULL); + } + case null, default -> throw new AnalyzerException(insn, "Illegal LDC value " + value); + } + case JSR: + return ObjectValue.VAL_JSR; + case GETSTATIC: + // TODO: Lookup for known static values (Integer.MAX for instance) + Type fieldType = Type.getType(((FieldInsnNode) insn).desc); + return newValue(fieldType); + case NEW: + Type objectType = Type.getObjectType(((TypeInsnNode) insn).desc); + return newValue(objectType, Nullness.NOT_NULL); + default: + throw new AssertionError(); + } + } + + @Override + public ReValue copyOperation(@Nonnull AbstractInsnNode insn, @Nonnull ReValue value) { + // Just keep the same value reference + return value; + } + + @Override + public ReValue unaryOperation(@Nonnull AbstractInsnNode insn, @Nonnull ReValue value) throws AnalyzerException { + switch (insn.getOpcode()) { + case IINC: { + int incr = ((IincInsnNode) insn).incr; + if (value instanceof IntValue iv) return iv.add(incr); + return IntValue.UNKNOWN; + } + case INEG: + if (value instanceof IntValue iv) return iv.negate(); + return IntValue.UNKNOWN; + case L2I: + if (value instanceof LongValue lv) return lv.castInt(); + return IntValue.UNKNOWN; + case F2I: + if (value instanceof FloatValue fv) return fv.castInt(); + return IntValue.UNKNOWN; + case D2I: + if (value instanceof DoubleValue dv) return dv.castInt(); + return IntValue.UNKNOWN; + case I2B: + if (value instanceof IntValue iv) return iv.castByte(); + return IntValue.UNKNOWN; + case I2C: + if (value instanceof IntValue iv) return iv.castChar(); + return IntValue.UNKNOWN; + case I2S: + if (value instanceof IntValue iv) return iv.castShort(); + return IntValue.UNKNOWN; + case FNEG: + if (value instanceof FloatValue fv) return fv.negate(); + return FloatValue.UNKNOWN; + case I2F: + if (value instanceof IntValue iv) return iv.castFloat(); + return FloatValue.UNKNOWN; + case L2F: + if (value instanceof LongValue lv) return lv.castFloat(); + return FloatValue.UNKNOWN; + case D2F: + if (value instanceof DoubleValue dv) return dv.castFloat(); + return FloatValue.UNKNOWN; + case LNEG: + if (value instanceof LongValue lv) return lv.negate(); + return LongValue.UNKNOWN; + case I2L: + if (value instanceof IntValue iv) return iv.castLong(); + return LongValue.UNKNOWN; + case F2L: + if (value instanceof FloatValue fv) return fv.castLong(); + return LongValue.UNKNOWN; + case D2L: + if (value instanceof DoubleValue dv) return dv.castLong(); + return LongValue.UNKNOWN; + case DNEG: + if (value instanceof DoubleValue dv) return dv.negate(); + return DoubleValue.UNKNOWN; + case I2D: + if (value instanceof IntValue iv) return iv.castDouble(); + return DoubleValue.UNKNOWN; + case L2D: + if (value instanceof LongValue lv) return lv.castDouble(); + return DoubleValue.UNKNOWN; + case F2D: + if (value instanceof FloatValue fv) return fv.castDouble(); + return DoubleValue.UNKNOWN; + case IFEQ: + case IFNE: + case IFLT: + case IFGE: + case IFGT: + case IFLE: + case TABLESWITCH: + case LOOKUPSWITCH: + case IRETURN: + case LRETURN: + case FRETURN: + case DRETURN: + case ARETURN: + case PUTSTATIC: + return null; + case MONITORENTER: + case MONITOREXIT: + case IFNULL: + case IFNONNULL: + return null; + case ATHROW: + return null; + case GETFIELD: { + Type fieldType = Type.getType(((FieldInsnNode) insn).desc); + return newValue(fieldType); + } + case NEWARRAY: + int arrayKind = ((IntInsnNode) insn).operand; + if (value instanceof IntValue length) { + OptionalInt lengthValue = length.value(); + if (lengthValue.isPresent()) { + Type type = switch (arrayKind) { + case T_BOOLEAN -> Types.ARRAY_1D_BOOLEAN; + case T_CHAR -> Types.ARRAY_1D_CHAR; + case T_BYTE -> Types.ARRAY_1D_BYTE; + case T_SHORT -> Types.ARRAY_1D_SHORT; + case T_INT -> Types.ARRAY_1D_INT; + case T_FLOAT -> Types.ARRAY_1D_FLOAT; + case T_DOUBLE -> Types.ARRAY_1D_DOUBLE; + case T_LONG -> Types.ARRAY_1D_LONG; + default -> throw new AnalyzerException(insn, "Invalid array type"); + }; + return ArrayValue.of(type, Nullness.NOT_NULL, lengthValue.getAsInt()); + } + } + return switch (arrayKind) { + case T_BOOLEAN -> ArrayValue.VAL_BOOLEANS; + case T_CHAR -> ArrayValue.VAL_CHARS; + case T_BYTE -> ArrayValue.VAL_BYTES; + case T_SHORT -> ArrayValue.VAL_SHORTS; + case T_INT -> ArrayValue.VAL_INTS; + case T_FLOAT -> ArrayValue.VAL_FLOATS; + case T_DOUBLE -> ArrayValue.VAL_DOUBLES; + case T_LONG -> ArrayValue.VAL_LONGS; + default -> throw new AnalyzerException(insn, "Invalid array type"); + }; + case ANEWARRAY: { + Type arrayType = Type.getType("[" + Type.getObjectType(((TypeInsnNode) insn).desc)); + return newValue(arrayType, Nullness.NOT_NULL); + } + case ARRAYLENGTH: + if (value instanceof ArrayValue array) { + OptionalInt firstDimensionLength = array.getFirstDimensionLength(); + if (firstDimensionLength.isPresent()) + return IntValue.of(firstDimensionLength.getAsInt()); + } + return IntValue.UNKNOWN; + case CHECKCAST: + Type targetType = Type.getObjectType(((TypeInsnNode) insn).desc); + return newValue(targetType); + case INSTANCEOF: + return IntValue.UNKNOWN; + default: + throw new AnalyzerException(insn, "Unknown unary op: " + insn.getOpcode()); + } + } + + @Override + public ReValue binaryOperation(@Nonnull AbstractInsnNode insn, @Nonnull ReValue value1, @Nonnull ReValue value2) { + switch (insn.getOpcode()) { + case IALOAD: + case BALOAD: + case CALOAD: + case SALOAD: + // We aren't tracking array contents, so nothing to do here. + return IntValue.UNKNOWN; + case FALOAD: + // We aren't tracking array contents, so nothing to do here. + return FloatValue.UNKNOWN; + case LALOAD: + // We aren't tracking array contents, so nothing to do here. + return LongValue.UNKNOWN; + case DALOAD: + // We aren't tracking array contents, so nothing to do here. + return DoubleValue.UNKNOWN; + case AALOAD: + // We aren't tracking array contents, so nothing to do here. + return ObjectValue.VAL_OBJECT_MAYBE_NULL; + case IADD: + if (value1 instanceof IntValue i1 && value2 instanceof IntValue i2) return i1.add(i2); + return IntValue.UNKNOWN; + case ISUB: + if (value1 instanceof IntValue i1 && value2 instanceof IntValue i2) return i1.sub(i2); + return IntValue.UNKNOWN; + case IMUL: + if (value1 instanceof IntValue i1 && value2 instanceof IntValue i2) return i1.mul(i2); + return IntValue.UNKNOWN; + case IDIV: + if (value1 instanceof IntValue i1 && value2 instanceof IntValue i2) return i1.div(i2); + return IntValue.UNKNOWN; + case IREM: + if (value1 instanceof IntValue i1 && value2 instanceof IntValue i2) return i1.rem(i2); + return IntValue.UNKNOWN; + case ISHL: + if (value1 instanceof IntValue i1 && value2 instanceof IntValue i2) return i1.shl(i2); + return IntValue.UNKNOWN; + case ISHR: + if (value1 instanceof IntValue i1 && value2 instanceof IntValue i2) return i1.shr(i2); + return IntValue.UNKNOWN; + case IUSHR: + if (value1 instanceof IntValue i1 && value2 instanceof IntValue i2) return i1.ushr(i2); + return IntValue.UNKNOWN; + case IAND: + if (value1 instanceof IntValue i1 && value2 instanceof IntValue i2) return i1.and(i2); + return IntValue.UNKNOWN; + case IOR: + if (value1 instanceof IntValue i1 && value2 instanceof IntValue i2) return i1.or(i2); + return IntValue.UNKNOWN; + case IXOR: + if (value1 instanceof IntValue i1 && value2 instanceof IntValue i2) return i1.xor(i2); + return IntValue.UNKNOWN; + case FADD: + if (value1 instanceof FloatValue f1 && value2 instanceof FloatValue f2) return f1.add(f2); + return FloatValue.UNKNOWN; + case FSUB: + if (value1 instanceof FloatValue f1 && value2 instanceof FloatValue f2) return f1.sub(f2); + return FloatValue.UNKNOWN; + case FMUL: + if (value1 instanceof FloatValue f1 && value2 instanceof FloatValue f2) return f1.mul(f2); + return FloatValue.UNKNOWN; + case FDIV: + if (value1 instanceof FloatValue f1 && value2 instanceof FloatValue f2) return f1.div(f2); + return FloatValue.UNKNOWN; + case FREM: + if (value1 instanceof FloatValue f1 && value2 instanceof FloatValue f2) return f1.rem(f2); + return FloatValue.UNKNOWN; + case LADD: + if (value1 instanceof LongValue l1 && value2 instanceof LongValue l2) return l1.add(l2); + return LongValue.UNKNOWN; + case LSUB: + if (value1 instanceof LongValue l1 && value2 instanceof LongValue l2) return l1.sub(l2); + return LongValue.UNKNOWN; + case LMUL: + if (value1 instanceof LongValue l1 && value2 instanceof LongValue l2) return l1.mul(l2); + return LongValue.UNKNOWN; + case LDIV: + if (value1 instanceof LongValue l1 && value2 instanceof LongValue l2) return l1.div(l2); + return LongValue.UNKNOWN; + case LREM: + if (value1 instanceof LongValue l1 && value2 instanceof LongValue l2) return l1.rem(l2); + return LongValue.UNKNOWN; + case LSHL: + if (value1 instanceof LongValue l1 && value2 instanceof IntValue l2) return l1.shl(l2); + if (value1 instanceof LongValue l1 && value2 instanceof LongValue l2) return l1.shl(l2); + return LongValue.UNKNOWN; + case LSHR: + if (value1 instanceof LongValue l1 && value2 instanceof IntValue l2) return l1.shr(l2); + if (value1 instanceof LongValue l1 && value2 instanceof LongValue l2) return l1.shr(l2); + return LongValue.UNKNOWN; + case LUSHR: + if (value1 instanceof LongValue l1 && value2 instanceof IntValue l2) return l1.ushr(l2); + if (value1 instanceof LongValue l1 && value2 instanceof LongValue l2) return l1.ushr(l2); + return LongValue.UNKNOWN; + case LAND: + if (value1 instanceof LongValue l1 && value2 instanceof LongValue l2) return l1.and(l2); + return LongValue.UNKNOWN; + case LOR: + if (value1 instanceof LongValue l1 && value2 instanceof LongValue l2) return l1.or(l2); + return LongValue.UNKNOWN; + case LXOR: + if (value1 instanceof LongValue l1 && value2 instanceof LongValue l2) return l1.xor(l2); + return LongValue.UNKNOWN; + case DADD: + if (value1 instanceof DoubleValue d1 && value2 instanceof DoubleValue d2) return d1.add(d2); + return DoubleValue.UNKNOWN; + case DSUB: + if (value1 instanceof DoubleValue d1 && value2 instanceof DoubleValue d2) return d1.sub(d2); + return DoubleValue.UNKNOWN; + case DMUL: + if (value1 instanceof DoubleValue d1 && value2 instanceof DoubleValue d2) return d1.mul(d2); + return DoubleValue.UNKNOWN; + case DDIV: + if (value1 instanceof DoubleValue d1 && value2 instanceof DoubleValue d2) return d1.div(d2); + return DoubleValue.UNKNOWN; + case DREM: + if (value1 instanceof DoubleValue d1 && value2 instanceof DoubleValue d2) return d1.rem(d2); + return DoubleValue.UNKNOWN; + case LCMP: + if (value1 instanceof LongValue l1 && value2 instanceof LongValue l2) return l1.cmp(l2); + return IntValue.UNKNOWN; + case FCMPL: + if (value1 instanceof FloatValue f1 && value2 instanceof FloatValue f2) return f1.cmpl(f2); + return IntValue.UNKNOWN; + case FCMPG: + if (value1 instanceof FloatValue f1 && value2 instanceof FloatValue f2) return f1.cmpg(f2); + return IntValue.UNKNOWN; + case DCMPL: + if (value1 instanceof DoubleValue d1 && value2 instanceof DoubleValue d2) return d1.cmpl(d2); + return IntValue.UNKNOWN; + case DCMPG: + if (value1 instanceof DoubleValue d1 && value2 instanceof DoubleValue d2) return d1.cmpg(d2); + return IntValue.UNKNOWN; + case IF_ICMPEQ: + case IF_ICMPNE: + case IF_ICMPLT: + case IF_ICMPGE: + case IF_ICMPGT: + case IF_ICMPLE: + case IF_ACMPEQ: + case IF_ACMPNE: + case PUTFIELD: + // Just popping values, not pushing anything back onto the stack + return null; + default: + throw new AssertionError(); + } + } + + @Override + public ReValue ternaryOperation(@Nonnull AbstractInsnNode insn, @Nonnull ReValue value1, @Nonnull ReValue value2, ReValue value3) { + // We don't track array operations, but this would cover: + // IASTORE, LASTORE, FASTORE, DASTORE, AASTORE, BASTORE, CASTORE, SASTORE + return null; + } + + @Override + public ReValue naryOperation(@Nonnull AbstractInsnNode insn, @Nonnull List values) { + int opcode = insn.getOpcode(); + if (opcode == MULTIANEWARRAY) { + Type type = Type.getType(((MultiANewArrayInsnNode) insn).desc); + return newValue(type, Nullness.NOT_NULL); + } else if (opcode == INVOKEDYNAMIC) { + Type returnType = Type.getReturnType(((InvokeDynamicInsnNode) insn).desc); + return newValue(returnType); + } else { + // TODO: Handle special case static methods + // TODO: Handle special case virtual methods on known string values + Type returnType = Type.getReturnType(((MethodInsnNode) insn).desc); + return newValue(returnType); + } + } + + @Override + public void returnOperation(@Nonnull AbstractInsnNode insn, @Nonnull ReValue value, @Nonnull ReValue expected) { + // no-op + } + + /** + * {@inheritDoc} + *

+ * Implementation adapted from {@link SimpleVerifier}. + * + * @param value1 + * A value. + * @param value2 + * Another value. + * + * @return The merged value. + */ + @Override + public ReValue merge(@Nonnull ReValue value1, @Nonnull ReValue value2) { + Type type1 = value1.type(); + Type type2 = value2.type(); + + // Null types correspond to UNINITIALIZED_VALUE. + if (type1 == null || type2 == null) + return UninitializedValue.UNINITIALIZED_VALUE; + if (type1.equals(type2)) + return value1; + + // The merge of a primitive type with a different type is the type of uninitialized values. + if (type1.getSort() != Type.OBJECT && type1.getSort() != Type.ARRAY) + return UninitializedValue.UNINITIALIZED_VALUE; + if (type2.getSort() != Type.OBJECT && type2.getSort() != Type.ARRAY) + return UninitializedValue.UNINITIALIZED_VALUE; + + // Special case for the type of the "null" literal. + if (value1 instanceof ObjectValue ov1 && ov1.isNull()) + return value2; + if (value2 instanceof ObjectValue ov2 && ov2.isNull()) + return value1; + + // Convert type1 to its element type and array dimension. Arrays of primitive values are seen as + // Object arrays with one dimension less. Hence, the element type is always of Type.OBJECT sort. + int dim1 = 0; + if (type1.getSort() == Type.ARRAY) { + dim1 = type1.getDimensions(); + type1 = type1.getElementType(); + if (type1.getSort() != Type.OBJECT) { + dim1 = dim1 - 1; + type1 = Types.OBJECT_TYPE; + } + } + + // Do the same for type2. + int dim2 = 0; + if (type2.getSort() == Type.ARRAY) { + dim2 = type2.getDimensions(); + type2 = type2.getElementType(); + if (type2.getSort() != Type.OBJECT) { + dim2 = dim2 - 1; + type2 = Types.OBJECT_TYPE; + } + } + + // The merge of array types of different dimensions is an Object array type. + if (dim1 != dim2) + return newArrayValue(Types.OBJECT_TYPE, Math.min(dim1, dim2)); + + // Type1 and type2 have a Type.OBJECT sort by construction (see above), + // as expected by isAssignableFrom. + if (isAssignableFrom(type1, type2)) + return newArrayValue(type1, dim1); + if (isAssignableFrom(type2, type1)) + return newArrayValue(type2, dim1); + + if (!isInterface(type1)) { + while (!Types.OBJECT_TYPE.equals(type1)) { + type1 = getSuperClass(type1); + if (isAssignableFrom(type1, type2)) + return newArrayValue(type1, dim1); + } + } + + return newArrayValue(Types.OBJECT_TYPE, dim1); + } + + @Nonnull + private Type getSuperClass(@Nonnull Type type) { + String name = type.getInternalName(); + InheritanceVertex vertex = graph.getVertex(name); + if (vertex == null) + return Types.OBJECT_TYPE; + String superName = vertex.getValue().getSuperName(); + return superName == null ? Types.OBJECT_TYPE : Type.getObjectType(superName); + } + + private boolean isInterface(@Nonnull Type type) { + String name = type.getInternalName(); + InheritanceVertex vertex = graph.getVertex(name); + if (vertex == null) + return false; + return vertex.getValue().hasInterfaceModifier(); + } + + private boolean isAssignableFrom(@Nonnull Type type1, @Nonnull Type type2) { + String name1 = type1.getInternalName(); + String name2 = type2.getInternalName(); + return graph.isAssignableFrom(name1, name2); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/ArrayValue.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/ArrayValue.java new file mode 100644 index 000000000..c037f274f --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/ArrayValue.java @@ -0,0 +1,117 @@ +package software.coley.recaf.util.analysis.value; + +import jakarta.annotation.Nonnull; +import org.objectweb.asm.Type; +import software.coley.recaf.util.analysis.Nullness; +import software.coley.recaf.util.analysis.value.impl.ArrayValueImpl; + +import java.util.OptionalInt; + +/** + * Value capable of recording partial details of array content. + * + * @author Matt Coley + */ +public interface ArrayValue extends ObjectValue { + ArrayValue VAL_BOOLEANS = new ArrayValueImpl(Type.getType("[Z"), Nullness.NOT_NULL); + ArrayValue VAL_CHARS = new ArrayValueImpl(Type.getType("[C"), Nullness.NOT_NULL); + ArrayValue VAL_BYTES = new ArrayValueImpl(Type.getType("[B"), Nullness.NOT_NULL); + ArrayValue VAL_SHORTS = new ArrayValueImpl(Type.getType("[S"), Nullness.NOT_NULL); + ArrayValue VAL_INTS = new ArrayValueImpl(Type.getType("[I"), Nullness.NOT_NULL); + ArrayValue VAL_FLOATS = new ArrayValueImpl(Type.getType("[F"), Nullness.NOT_NULL); + ArrayValue VAL_DOUBLES = new ArrayValueImpl(Type.getType("[D"), Nullness.NOT_NULL); + ArrayValue VAL_LONGS = new ArrayValueImpl(Type.getType("[J"), Nullness.NOT_NULL); + + /** + * @param type + * Array type. + * @param nullness + * Array null state. + * + * @return Array value holding the array content. + */ + @Nonnull + static ArrayValue of(@Nonnull Type type, @Nonnull Nullness nullness) { + String descriptor = type.getDescriptor(); + return switch (descriptor) { + case "[Z" -> VAL_BOOLEANS; + case "[C" -> VAL_CHARS; + case "[B" -> VAL_BYTES; + case "[S" -> VAL_SHORTS; + case "[I" -> VAL_INTS; + case "[F" -> VAL_FLOATS; + case "[D" -> VAL_DOUBLES; + case "[J" -> VAL_LONGS; + default -> new ArrayValueImpl(type, nullness); + }; + } + + /** + * @param type + * Array type. + * @param nullness + * Array null state. + * @param length + * Array length. + * + * @return Array value holding the array content. + */ + @Nonnull + static ArrayValue of(@Nonnull Type type, @Nonnull Nullness nullness, int length) { + return new ArrayValueImpl(type, nullness, length); + } + + @Override + default boolean hasKnownValue() { + return false; + } + + @Nonnull + @Override + Type type(); + + /** + * The element type is the base of any array. Consider the following: + *

    + *
  • {@code int[]}
  • + *
  • {@code int[][]}
  • + *
  • {@code int[][][]}
  • + *
+ * The element type of each is {@code int}. + * + * @return Element type of the array. + */ + @Nonnull + default Type elementType() { + return type().getElementType(); + } + + /** + * Consider the following: + *
    + *
  • 1: {@code int[]}
  • + *
  • 2: {@code int[][]}
  • + *
  • 3: {@code int[][][]}
  • + *
+ * + * @return Dimensions of the array. + */ + default int dimensions() { + return type().getDimensions(); + } + + /** + * Consider the following: + * + * + * + * + * + * + *
LengthArray definition
{@code 7}{@code int[7]}
{@code 7}{@code int[7][9]}
Unknown{@code int[][9]}
Unknown{@code int[]}
+ * + * @return Length of the first dimension. + */ + @Nonnull + OptionalInt getFirstDimensionLength(); +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/DoubleValue.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/DoubleValue.java new file mode 100644 index 000000000..a765e5a16 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/DoubleValue.java @@ -0,0 +1,158 @@ +package software.coley.recaf.util.analysis.value; + +import jakarta.annotation.Nonnull; +import org.objectweb.asm.Type; +import software.coley.recaf.util.analysis.value.impl.DoubleValueImpl; + +import java.util.OptionalDouble; + +/** + * Value capable of recording exact floating double precision content. + * + * @author Matt Coley + */ +public non-sealed interface DoubleValue extends ReValue { + DoubleValue UNKNOWN = new DoubleValueImpl(); + DoubleValue VAL_MAX = new DoubleValueImpl(Double.MAX_VALUE); + DoubleValue VAL_MIN = new DoubleValueImpl(Double.MIN_VALUE); + DoubleValue VAL_M1 = new DoubleValueImpl(-1); + DoubleValue VAL_0 = new DoubleValueImpl(0); + DoubleValue VAL_1 = new DoubleValueImpl(1); + + /** + * @param value + * Double value to hold. + * + * @return Double value holding the exact content. + */ + @Nonnull + static DoubleValue of(double value) { + if (value == 0) return VAL_0; + else if (value == 1) return VAL_1; + else if (value == -1) return VAL_M1; + else if (value == Double.MAX_VALUE) return VAL_MAX; + else if (value == Double.MIN_VALUE) return VAL_MIN; + return new DoubleValueImpl(value); + } + + /** + * @return Double content of value. Empty if {@link #hasKnownValue() not known}. + */ + @Nonnull + OptionalDouble value(); + + @Override + default boolean hasKnownValue() { + return value().isPresent(); + } + + @Nonnull + @Override + default Type type() { + return Type.DOUBLE_TYPE; + } + + @Override + default int getSize() { + return 2; + } + + @Nonnull + default DoubleValue add(@Nonnull DoubleValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsDouble() + otherValue.getAsDouble()); + return UNKNOWN; + } + + @Nonnull + default DoubleValue sub(@Nonnull DoubleValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsDouble() - otherValue.getAsDouble()); + return UNKNOWN; + } + + @Nonnull + default DoubleValue mul(@Nonnull DoubleValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsDouble() * otherValue.getAsDouble()); + return UNKNOWN; + } + + @Nonnull + default DoubleValue div(@Nonnull DoubleValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) { + double otherLiteral = otherValue.getAsDouble(); + if (otherLiteral == 0) return UNKNOWN; // We'll just pretend this works + return of((value.getAsDouble() / otherLiteral)); + } + return UNKNOWN; + } + + @Nonnull + default IntValue cmpg(@Nonnull DoubleValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) { + double f1 = value.getAsDouble(); + double f2 = otherValue.getAsDouble(); + if (Double.isNaN(f1) || Double.isNaN(f2)) return IntValue.VAL_1; + return IntValue.of(Double.compare(f1, f2)); + } + return IntValue.UNKNOWN; + } + + @Nonnull + default IntValue cmpl(@Nonnull DoubleValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) { + double f1 = value.getAsDouble(); + double f2 = otherValue.getAsDouble(); + if (Double.isNaN(f1) || Double.isNaN(f2)) return IntValue.VAL_M1; + return IntValue.of(Double.compare(f1, f2)); + } + return IntValue.UNKNOWN; + } + + @Nonnull + default DoubleValue rem(@Nonnull DoubleValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) + return of((float) (value.getAsDouble() % otherValue.getAsDouble())); + return UNKNOWN; + } + + @Nonnull + default DoubleValue negate() { + OptionalDouble value = value(); + if (value.isPresent()) return of(-value.getAsDouble()); + return UNKNOWN; + } + + @Nonnull + default IntValue castInt() { + OptionalDouble value = value(); + if (value.isPresent()) return IntValue.of((int) value.getAsDouble()); + return IntValue.UNKNOWN; + } + + @Nonnull + default FloatValue castFloat() { + OptionalDouble value = value(); + if (value.isPresent()) return FloatValue.of((float) value.getAsDouble()); + return FloatValue.UNKNOWN; + } + + @Nonnull + default LongValue castLong() { + OptionalDouble value = value(); + if (value.isPresent()) return LongValue.of((long) value.getAsDouble()); + return LongValue.UNKNOWN; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/FloatValue.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/FloatValue.java new file mode 100644 index 000000000..32db759ba --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/FloatValue.java @@ -0,0 +1,165 @@ +package software.coley.recaf.util.analysis.value; + +import jakarta.annotation.Nonnull; +import org.objectweb.asm.Type; +import software.coley.recaf.util.analysis.value.impl.FloatValueImpl; + +import java.util.OptionalDouble; + +/** + * Value capable of recording exact floating point content. + * + * @author Matt Coley + */ +public non-sealed interface FloatValue extends ReValue { + FloatValue UNKNOWN = new FloatValueImpl(); + FloatValue VAL_MAX = new FloatValueImpl(Float.MAX_VALUE); + FloatValue VAL_MIN = new FloatValueImpl(Float.MIN_VALUE); + FloatValue VAL_M1 = new FloatValueImpl(-1); + FloatValue VAL_0 = new FloatValueImpl(0); + FloatValue VAL_1 = new FloatValueImpl(1); + FloatValue VAL_2 = new FloatValueImpl(2); + + /** + * @param value + * Float value to hold. + * + * @return Float value holding the exact content. + */ + @Nonnull + static FloatValue of(float value) { + if (value == 0) return VAL_0; + else if (value == 1) return VAL_1; + else if (value == -1) return VAL_M1; + else if (value == 2) return VAL_2; + else if (value == Float.MAX_VALUE) return VAL_MAX; + else if (value == Float.MIN_VALUE) return VAL_MIN; + return new FloatValueImpl(value); + } + + /** + * @return Float content of value. Empty if {@link #hasKnownValue() not known}. + * + * @implNote Java does not have an {@code OptionalFloat}. + */ + @Nonnull + OptionalDouble value(); + + @Override + default boolean hasKnownValue() { + return value().isPresent(); + } + + @Nonnull + @Override + default Type type() { + return Type.FLOAT_TYPE; + } + + @Override + default int getSize() { + return 1; + } + + @Nonnull + default FloatValue add(@Nonnull FloatValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) + return of((float) (value.getAsDouble() + otherValue.getAsDouble())); + return UNKNOWN; + } + + @Nonnull + default FloatValue sub(@Nonnull FloatValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) + return of((float) (value.getAsDouble() - otherValue.getAsDouble())); + return UNKNOWN; + } + + @Nonnull + default FloatValue mul(@Nonnull FloatValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) + return of((float) (value.getAsDouble() * otherValue.getAsDouble())); + return UNKNOWN; + } + + @Nonnull + default FloatValue div(@Nonnull FloatValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) { + double otherLiteral = otherValue.getAsDouble(); + if (otherLiteral == 0) return UNKNOWN; // We'll just pretend this works + return of((float) (value.getAsDouble() / otherLiteral)); + } + return UNKNOWN; + } + + @Nonnull + default IntValue cmpg(@Nonnull FloatValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) { + float f1 = (float) value.getAsDouble(); + float f2 = (float) otherValue.getAsDouble(); + if (Float.isNaN(f1) || Float.isNaN(f2)) return IntValue.VAL_1; + return IntValue.of(Float.compare(f1, f2)); + } + return IntValue.UNKNOWN; + } + + @Nonnull + default IntValue cmpl(@Nonnull FloatValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) { + float f1 = (float) value.getAsDouble(); + float f2 = (float) otherValue.getAsDouble(); + if (Float.isNaN(f1) || Float.isNaN(f2)) return IntValue.VAL_M1; + return IntValue.of(Float.compare(f1, f2)); + } + return IntValue.UNKNOWN; + } + + @Nonnull + default FloatValue rem(@Nonnull FloatValue other) { + OptionalDouble value = value(); + OptionalDouble otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) + return of((float) (value.getAsDouble() % otherValue.getAsDouble())); + return UNKNOWN; + } + + @Nonnull + default FloatValue negate() { + OptionalDouble value = value(); + if (value.isPresent()) return of((float) -value.getAsDouble()); + return UNKNOWN; + } + + @Nonnull + default IntValue castInt() { + OptionalDouble value = value(); + if (value.isPresent()) return IntValue.of((int) value.getAsDouble()); + return IntValue.UNKNOWN; + } + + @Nonnull + default DoubleValue castDouble() { + OptionalDouble value = value(); + if (value.isPresent()) return DoubleValue.of(value.getAsDouble()); + return DoubleValue.UNKNOWN; + } + + @Nonnull + default LongValue castLong() { + OptionalDouble value = value(); + if (value.isPresent()) return LongValue.of((long) value.getAsDouble()); + return LongValue.UNKNOWN; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/IntValue.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/IntValue.java new file mode 100644 index 000000000..0fa6f5a88 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/IntValue.java @@ -0,0 +1,217 @@ +package software.coley.recaf.util.analysis.value; + +import jakarta.annotation.Nonnull; +import org.objectweb.asm.Type; +import software.coley.recaf.util.analysis.value.impl.IntValueImpl; + +import java.util.OptionalInt; + +/** + * Value capable of recording exact integer content. + * + * @author Matt Coley + */ +public non-sealed interface IntValue extends ReValue { + IntValue UNKNOWN = new IntValueImpl(); + IntValue VAL_MAX = new IntValueImpl(Integer.MAX_VALUE); + IntValue VAL_MIN = new IntValueImpl(Integer.MIN_VALUE); + IntValue VAL_M1 = new IntValueImpl(-1); + IntValue VAL_0 = new IntValueImpl(0); + IntValue VAL_1 = new IntValueImpl(1); + IntValue VAL_2 = new IntValueImpl(2); + IntValue VAL_3 = new IntValueImpl(3); + IntValue VAL_4 = new IntValueImpl(4); + IntValue VAL_5 = new IntValueImpl(5); + + /** + * @param value + * Integer value to hold. + * + * @return Integer value holding the exact content. + */ + @Nonnull + static IntValue of(int value) { + return switch (value) { + case Integer.MAX_VALUE -> VAL_MAX; + case Integer.MIN_VALUE -> VAL_MIN; + case -1 -> VAL_M1; + case 0 -> VAL_0; + case 1 -> VAL_1; + case 2 -> VAL_2; + case 3 -> VAL_3; + case 4 -> VAL_4; + case 5 -> VAL_5; + default -> new IntValueImpl(value); + }; + } + + /** + * @return Integer content of value. Empty if {@link #hasKnownValue() not known}. + */ + @Nonnull + OptionalInt value(); + + @Override + default boolean hasKnownValue() { + return value().isPresent(); + } + + @Nonnull + @Override + default Type type() { + return Type.INT_TYPE; + } + + @Override + default int getSize() { + return 1; + } + + @Nonnull + default IntValue add(int incr) { + OptionalInt value = value(); + if (value.isPresent()) return of(value.getAsInt() + incr); + return UNKNOWN; + } + + @Nonnull + default IntValue add(@Nonnull IntValue other) { + OptionalInt value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsInt() + otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue sub(@Nonnull IntValue other) { + OptionalInt value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsInt() - otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue mul(@Nonnull IntValue other) { + OptionalInt value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsInt() * otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue div(@Nonnull IntValue other) { + OptionalInt value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) { + int otherLiteral = otherValue.getAsInt(); + if (otherLiteral == 0) return UNKNOWN; // We'll just pretend this works + return of(value.getAsInt() / otherLiteral); + } + return UNKNOWN; + } + + @Nonnull + default IntValue and(@Nonnull IntValue other) { + OptionalInt value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsInt() & otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue or(@Nonnull IntValue other) { + OptionalInt value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsInt() | otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue xor(@Nonnull IntValue other) { + OptionalInt value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsInt() ^ otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue rem(@Nonnull IntValue other) { + OptionalInt value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsInt() % otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue shl(@Nonnull IntValue other) { + OptionalInt value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsInt() << otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue shr(@Nonnull IntValue other) { + OptionalInt value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsInt() >> otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue ushr(@Nonnull IntValue other) { + OptionalInt value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsInt() >>> otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue negate() { + OptionalInt value = value(); + if (value.isPresent()) return of(value.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue castByte() { + OptionalInt value = value(); + if (value.isPresent()) return of((byte) value.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue castChar() { + OptionalInt value = value(); + if (value.isPresent()) return of((char) value.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default IntValue castShort() { + OptionalInt value = value(); + if (value.isPresent()) return of((short) value.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default FloatValue castFloat() { + OptionalInt value = value(); + if (value.isPresent()) return FloatValue.of(value.getAsInt()); + return FloatValue.UNKNOWN; + } + + @Nonnull + default DoubleValue castDouble() { + OptionalInt value = value(); + if (value.isPresent()) return DoubleValue.of(value.getAsInt()); + return DoubleValue.UNKNOWN; + } + + @Nonnull + default LongValue castLong() { + OptionalInt value = value(); + if (value.isPresent()) return LongValue.of(value.getAsInt()); + return LongValue.UNKNOWN; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/LongValue.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/LongValue.java new file mode 100644 index 000000000..4f5c24334 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/LongValue.java @@ -0,0 +1,221 @@ +package software.coley.recaf.util.analysis.value; + +import jakarta.annotation.Nonnull; +import org.objectweb.asm.Type; +import software.coley.recaf.util.analysis.value.impl.LongValueImpl; + +import java.util.OptionalInt; +import java.util.OptionalLong; + +/** + * Value capable of recording exact long content. + * + * @author Matt Coley + */ +public non-sealed interface LongValue extends ReValue { + LongValue UNKNOWN = new LongValueImpl(); + LongValue VAL_MAX = new LongValueImpl(Long.MAX_VALUE); + LongValue VAL_MIN = new LongValueImpl(Long.MIN_VALUE); + LongValue VAL_M1 = new LongValueImpl(-1); + LongValue VAL_0 = new LongValueImpl(0); + LongValue VAL_1 = new LongValueImpl(1); + + /** + * @param value + * Long value to hold. + * + * @return Long value holding the exact content. + */ + @Nonnull + static LongValue of(long value) { + if (value == 0) return VAL_0; + else if (value == 1) return VAL_1; + else if (value == -1) return VAL_M1; + else if (value == Long.MAX_VALUE) return VAL_MAX; + else if (value == Long.MIN_VALUE) return VAL_MIN; + return new LongValueImpl(value); + } + + /** + * @return Long content of value. Empty if {@link #hasKnownValue() not known}. + */ + @Nonnull + OptionalLong value(); + + @Override + default boolean hasKnownValue() { + return value().isPresent(); + } + + @Nonnull + @Override + default Type type() { + return Type.LONG_TYPE; + } + + @Override + default int getSize() { + return 2; + } + + @Nonnull + default LongValue add(@Nonnull LongValue other) { + OptionalLong value = value(); + OptionalLong otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() + otherValue.getAsLong()); + return UNKNOWN; + } + + @Nonnull + default LongValue sub(@Nonnull LongValue other) { + OptionalLong value = value(); + OptionalLong otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() - otherValue.getAsLong()); + return UNKNOWN; + } + + @Nonnull + default LongValue mul(@Nonnull LongValue other) { + OptionalLong value = value(); + OptionalLong otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() * otherValue.getAsLong()); + return UNKNOWN; + } + + @Nonnull + default LongValue div(@Nonnull LongValue other) { + OptionalLong value = value(); + OptionalLong otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) { + long otherLiteral = otherValue.getAsLong(); + if (otherLiteral == 0) return UNKNOWN; // We'll just pretend this works + return of(value.getAsLong() / otherLiteral); + } + return UNKNOWN; + } + + @Nonnull + default LongValue and(@Nonnull LongValue other) { + OptionalLong value = value(); + OptionalLong otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() & otherValue.getAsLong()); + return UNKNOWN; + } + + @Nonnull + default LongValue or(@Nonnull LongValue other) { + OptionalLong value = value(); + OptionalLong otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() | otherValue.getAsLong()); + return UNKNOWN; + } + + @Nonnull + default LongValue xor(@Nonnull LongValue other) { + OptionalLong value = value(); + OptionalLong otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() ^ otherValue.getAsLong()); + return UNKNOWN; + } + + @Nonnull + default IntValue cmp(@Nonnull LongValue other) { + OptionalLong value = value(); + OptionalLong otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) + return IntValue.of(Long.compare(value.getAsLong(), otherValue.getAsLong())); + return IntValue.UNKNOWN; + } + + @Nonnull + default LongValue rem(@Nonnull LongValue other) { + OptionalLong value = value(); + OptionalLong otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() % otherValue.getAsLong()); + return UNKNOWN; + } + + @Nonnull + default LongValue shl(@Nonnull LongValue other) { + OptionalLong value = value(); + OptionalLong otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() << otherValue.getAsLong()); + return UNKNOWN; + } + + @Nonnull + default LongValue shl(@Nonnull IntValue other) { + OptionalLong value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() << otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default LongValue shr(@Nonnull LongValue other) { + OptionalLong value = value(); + OptionalLong otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() >> otherValue.getAsLong()); + return UNKNOWN; + } + + + @Nonnull + default LongValue shr(@Nonnull IntValue other) { + OptionalLong value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() >> otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default LongValue ushr(@Nonnull LongValue other) { + OptionalLong value = value(); + OptionalLong otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() >>> otherValue.getAsLong()); + return UNKNOWN; + } + + @Nonnull + default LongValue ushr(@Nonnull IntValue other) { + OptionalLong value = value(); + OptionalInt otherValue = other.value(); + if (value.isPresent() && otherValue.isPresent()) return of(value.getAsLong() >>> otherValue.getAsInt()); + return UNKNOWN; + } + + @Nonnull + default LongValue negate() { + OptionalLong value = value(); + if (value.isPresent()) return of(-value.getAsLong()); + return UNKNOWN; + } + + @Nonnull + default LongValue add(long incr) { + OptionalLong value = value(); + if (value.isPresent()) return of(value.getAsLong() + incr); + return UNKNOWN; + } + + @Nonnull + default IntValue castInt() { + OptionalLong value = value(); + if (value.isPresent()) return IntValue.of((int) value.getAsLong()); + return IntValue.UNKNOWN; + } + + @Nonnull + default FloatValue castFloat() { + OptionalLong value = value(); + if (value.isPresent()) return FloatValue.of(value.getAsLong()); + return FloatValue.UNKNOWN; + } + + @Nonnull + default DoubleValue castDouble() { + OptionalLong value = value(); + if (value.isPresent()) return DoubleValue.of(value.getAsLong()); + return DoubleValue.UNKNOWN; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/ObjectValue.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/ObjectValue.java new file mode 100644 index 000000000..94d77fd4a --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/ObjectValue.java @@ -0,0 +1,95 @@ +package software.coley.recaf.util.analysis.value; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.objectweb.asm.Type; +import software.coley.recaf.util.Types; +import software.coley.recaf.util.analysis.Nullness; +import software.coley.recaf.util.analysis.value.impl.ObjectValueImpl; +import software.coley.recaf.util.analysis.value.impl.StringValueImpl; + +/** + * Value capable of recording exact content of certain object types. + * + * + * + * + * + * + *
ContentValue usage
{@code null}{@link #VAL_OBJECT_NULL}
{@code Any.class}{@link #VAL_CLASS}
{@code Method::reference}{@link #VAL_METHOD_HANDLE}
{@code "strings"}{@link #string(String)} or {@link #string(Nullness)}
+ * + * @author Matt Coley + */ +public non-sealed interface ObjectValue extends ReValue { + ObjectValue VAL_OBJECT = new ObjectValueImpl(Types.OBJECT_TYPE, Nullness.NOT_NULL); + ObjectValue VAL_OBJECT_NULL = new ObjectValueImpl(Types.OBJECT_TYPE, Nullness.NULL); + ObjectValue VAL_OBJECT_MAYBE_NULL = new ObjectValueImpl(Types.OBJECT_TYPE, Nullness.UNKNOWN); + ObjectValue VAL_CLASS = new ObjectValueImpl(Types.CLASS_TYPE, Nullness.NOT_NULL); + ObjectValue VAL_CLASS_NULL = new ObjectValueImpl(Types.CLASS_TYPE, Nullness.NULL); + ObjectValue VAL_CLASS_MAYBE_NULL = new ObjectValueImpl(Types.CLASS_TYPE, Nullness.NULL); + ObjectValue VAL_METHOD_TYPE = new ObjectValueImpl(Type.getObjectType("java/lang/invoke/MethodType"), Nullness.NOT_NULL); + ObjectValue VAL_METHOD_HANDLE = new ObjectValueImpl(Type.getObjectType("java/lang/invoke/MethodType"), Nullness.NOT_NULL); + ObjectValue VAL_JSR = new ObjectValueImpl(Type.VOID_TYPE, Nullness.NOT_NULL); + + /** + * @param text + * Exact string content. + * + * @return String value holding the exact content. + */ + @Nonnull + static StringValue string(@Nullable String text) { + if (text == null) return StringValue.VAL_STRING_NULL; + if (text.isEmpty()) return StringValue.VAL_STRING_EMPTY; + if (text.equals(" ")) return StringValue.VAL_STRING_SPACE; + return new StringValueImpl(text); + } + + /** + * @param nullness + * Null state of the string. + * + * @return String value of the given nullness. + */ + @Nonnull + static StringValue string(@Nonnull Nullness nullness) { + return switch (nullness) { + case NULL -> StringValue.VAL_STRING_NULL; + case NOT_NULL -> StringValue.VAL_STRING; + case UNKNOWN -> StringValue.VAL_STRING_MAYBE_NULL; + }; + } + + /** + * @param nullness + * Null state of the string. + * + * @return Object value of the given nullness with a type of {@link Object}. + */ + @Nonnull + static ObjectValue object(@Nonnull Nullness nullness) { + return switch (nullness) { + case NULL -> VAL_OBJECT_NULL; + case NOT_NULL -> VAL_OBJECT; + case UNKNOWN -> VAL_OBJECT_MAYBE_NULL; + }; + } + + /** + * @return Null state of this value. + */ + @Nonnull + Nullness nullness(); + + /** + * @return {@code true} when this value is known to be {@code null}. + */ + default boolean isNull() { + return nullness() == Nullness.NULL; + } + + @Override + default int getSize() { + return 1; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/ReValue.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/ReValue.java new file mode 100644 index 000000000..029fa3b89 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/ReValue.java @@ -0,0 +1,24 @@ +package software.coley.recaf.util.analysis.value; + +import jakarta.annotation.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.analysis.Value; + +/** + * Base type of value capable of recording exact content + * when all control flow paths converge on a single use case. + * + * @author Matt Coley + */ +public sealed interface ReValue extends Value permits IntValue, FloatValue, DoubleValue, LongValue, ObjectValue, UninitializedValue { + /** + * @return {@code true} when the exact content is known. + */ + boolean hasKnownValue(); + + /** + * @return Type of value content. + */ + @Nullable + Type type(); +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/StringValue.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/StringValue.java new file mode 100644 index 000000000..7535f6d2c --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/StringValue.java @@ -0,0 +1,26 @@ +package software.coley.recaf.util.analysis.value; + +import jakarta.annotation.Nonnull; +import software.coley.recaf.util.analysis.Nullness; +import software.coley.recaf.util.analysis.value.impl.StringValueImpl; + +import java.util.Optional; + +/** + * Value capable of recording exact content of strings. + * + * @author Matt Coley + */ +public interface StringValue extends ObjectValue { + StringValue VAL_STRING = new StringValueImpl(Nullness.NOT_NULL); + StringValue VAL_STRING_MAYBE_NULL = new StringValueImpl(Nullness.UNKNOWN); + StringValue VAL_STRING_NULL = new StringValueImpl(Nullness.NULL); + StringValue VAL_STRING_EMPTY = new StringValueImpl(""); + StringValue VAL_STRING_SPACE = new StringValueImpl(" "); + + /** + * @return String content of value. Empty if {@link #hasKnownValue() not known}. + */ + @Nonnull + Optional getText(); +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/UninitializedValue.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/UninitializedValue.java new file mode 100644 index 000000000..efc5d0c4d --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/UninitializedValue.java @@ -0,0 +1,30 @@ +package software.coley.recaf.util.analysis.value; + +import jakarta.annotation.Nullable; +import org.objectweb.asm.Type; +import software.coley.recaf.util.analysis.value.impl.UninitializedValueImpl; + +/** + * Value representing a slot that has not been initialized or used yet. + * + * @author Matt Coley + */ +public non-sealed interface UninitializedValue extends ReValue { + UninitializedValue UNINITIALIZED_VALUE = UninitializedValueImpl.UNINITIALIZED_VALUE; + + @Override + default boolean hasKnownValue() { + return false; + } + + @Nullable + @Override + default Type type() { + return null; + } + + @Override + default int getSize() { + return 1; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/ArrayValueImpl.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/ArrayValueImpl.java new file mode 100644 index 000000000..ad07c714f --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/ArrayValueImpl.java @@ -0,0 +1,75 @@ +package software.coley.recaf.util.analysis.value.impl; + +import jakarta.annotation.Nonnull; +import org.objectweb.asm.Type; +import software.coley.recaf.util.analysis.Nullness; +import software.coley.recaf.util.analysis.value.ArrayValue; + +import java.sql.Types; +import java.util.OptionalInt; + +/** + * Array value holder implementation. + * + * @author Matt Coley + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class ArrayValueImpl implements ArrayValue { + private final Type type; + private final Nullness nullness; + private final OptionalInt length; + + public ArrayValueImpl(@Nonnull Type type, @Nonnull Nullness nullness) { + if (type.getSort() != Types.ARRAY) throw new IllegalStateException("Non-array type passed to array-value"); + this.type = type; + this.nullness = nullness; + this.length = OptionalInt.empty(); + } + + public ArrayValueImpl(@Nonnull Type type, @Nonnull Nullness nullness, int length) { + if (type.getSort() != Types.ARRAY) throw new IllegalStateException("Non-array type passed to array-value"); + this.type = type; + this.nullness = nullness; + this.length = OptionalInt.of(length); + } + + @Nonnull + @Override + public Type type() { + return type; + } + + @Nonnull + @Override + public Nullness nullness() { + return nullness; + } + + @Nonnull + @Override + public OptionalInt getFirstDimensionLength() { + return length; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ArrayValueImpl other = (ArrayValueImpl) o; + + return type.equals(other.type) && nullness.equals(other.nullness); + } + + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + nullness.hashCode(); + return result; + } + + @Override + public String toString() { + return type().getInternalName(); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/DoubleValueImpl.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/DoubleValueImpl.java new file mode 100644 index 000000000..e2a7ff3ec --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/DoubleValueImpl.java @@ -0,0 +1,50 @@ +package software.coley.recaf.util.analysis.value.impl; + +import jakarta.annotation.Nonnull; +import software.coley.recaf.util.analysis.value.DoubleValue; + +import java.util.OptionalDouble; + +/** + * Double value holder implementation. + * + * @author Matt Coley + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class DoubleValueImpl implements DoubleValue { + private final OptionalDouble value; + + public DoubleValueImpl(double value) { + this.value = OptionalDouble.of(value); + } + + public DoubleValueImpl() { + this.value = OptionalDouble.empty(); + } + + @Nonnull + @Override + public OptionalDouble value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DoubleValueImpl other = (DoubleValueImpl) o; + + return value.equals(other.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return type().getInternalName() + ":" + (value.isPresent() ? value.getAsDouble() : "?"); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/FloatValueImpl.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/FloatValueImpl.java new file mode 100644 index 000000000..7649fd165 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/FloatValueImpl.java @@ -0,0 +1,54 @@ +package software.coley.recaf.util.analysis.value.impl; + +import jakarta.annotation.Nonnull; +import software.coley.recaf.util.analysis.value.FloatValue; + +import java.util.OptionalDouble; + +/** + * Float value holder implementation. + * + * @author Matt Coley + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class FloatValueImpl implements FloatValue { + private final OptionalDouble value; + + public FloatValueImpl(float value) { + this.value = OptionalDouble.of(value); + } + + public FloatValueImpl(double value) { + this.value = OptionalDouble.of(value); + } + + public FloatValueImpl() { + this.value = OptionalDouble.empty(); + } + + @Nonnull + @Override + public OptionalDouble value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FloatValueImpl other = (FloatValueImpl) o; + + return value.equals(other.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return type().getInternalName() + ":" + (value.isPresent() ? value.getAsDouble() : "?"); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/IntValueImpl.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/IntValueImpl.java new file mode 100644 index 000000000..2965e5f14 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/IntValueImpl.java @@ -0,0 +1,50 @@ +package software.coley.recaf.util.analysis.value.impl; + +import jakarta.annotation.Nonnull; +import software.coley.recaf.util.analysis.value.IntValue; + +import java.util.OptionalInt; + +/** + * Integer value holder implementation. + * + * @author Matt Coley + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class IntValueImpl implements IntValue { + private final OptionalInt value; + + public IntValueImpl() { + this.value = OptionalInt.empty(); + } + + public IntValueImpl(int value) { + this.value = OptionalInt.of(value); + } + + @Nonnull + @Override + public OptionalInt value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + IntValueImpl other = (IntValueImpl) o; + + return value.equals(other.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return type().getInternalName() + ":" + (value.isPresent() ? value.getAsInt() : "?"); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/LongValueImpl.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/LongValueImpl.java new file mode 100644 index 000000000..ece109b89 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/LongValueImpl.java @@ -0,0 +1,50 @@ +package software.coley.recaf.util.analysis.value.impl; + +import jakarta.annotation.Nonnull; +import software.coley.recaf.util.analysis.value.LongValue; + +import java.util.OptionalLong; + +/** + * Long value holder implementation. + * + * @author Matt Coley + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class LongValueImpl implements LongValue { + private final OptionalLong value; + + public LongValueImpl(long value) { + this.value = OptionalLong.of(value); + } + + public LongValueImpl() { + this.value = OptionalLong.empty(); + } + + @Nonnull + @Override + public OptionalLong value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + LongValueImpl other = (LongValueImpl) o; + + return value.equals(other.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return type().getInternalName() + ":" + (value.isPresent() ? value.getAsLong() : "?"); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/ObjectValueImpl.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/ObjectValueImpl.java new file mode 100644 index 000000000..6e0f0d74d --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/ObjectValueImpl.java @@ -0,0 +1,60 @@ +package software.coley.recaf.util.analysis.value.impl; + +import jakarta.annotation.Nonnull; +import org.objectweb.asm.Type; +import software.coley.recaf.util.analysis.Nullness; +import software.coley.recaf.util.analysis.value.ObjectValue; + +/** + * Object value holder implementation. + * + * @author Matt Coley + */ +public class ObjectValueImpl implements ObjectValue { + private final Type type; + private final Nullness nullness; + + public ObjectValueImpl(@Nonnull Type type, @Nonnull Nullness nullness) { + this.type = type; + this.nullness = nullness; + } + + @Override + public boolean hasKnownValue() { + return false; + } + + @Nonnull + @Override + public Type type() { + return type; + } + + @Nonnull + @Override + public Nullness nullness() { + return nullness; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ObjectValueImpl other = (ObjectValueImpl) o; + + return type.equals(other.type) && nullness.equals(other.nullness); + } + + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + nullness.hashCode(); + return result; + } + + @Override + public String toString() { + return type.getInternalName(); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/StringValueImpl.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/StringValueImpl.java new file mode 100644 index 000000000..b088ef78c --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/StringValueImpl.java @@ -0,0 +1,40 @@ +package software.coley.recaf.util.analysis.value.impl; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import software.coley.recaf.util.Types; +import software.coley.recaf.util.analysis.Nullness; +import software.coley.recaf.util.analysis.value.StringValue; + +import java.util.Optional; + +/** + * String value holder implementation. + * + * @author Matt Coley + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class StringValueImpl extends ObjectValueImpl implements StringValue { + private final Optional text; + + public StringValueImpl(@Nonnull Nullness nullness) { + super(Types.STRING_TYPE, nullness); + text = Optional.empty(); + } + + public StringValueImpl(@Nullable String text) { + super(Types.STRING_TYPE, text != null ? Nullness.NOT_NULL : Nullness.UNKNOWN); + this.text = Optional.ofNullable(text); + } + + @Nonnull + @Override + public Optional getText() { + return text; + } + + @Override + public boolean hasKnownValue() { + return nullness() == Nullness.NOT_NULL && text.isPresent(); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/UninitializedValueImpl.java b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/UninitializedValueImpl.java new file mode 100644 index 000000000..4f38e8e21 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/analysis/value/impl/UninitializedValueImpl.java @@ -0,0 +1,19 @@ +package software.coley.recaf.util.analysis.value.impl; + +import software.coley.recaf.util.analysis.value.UninitializedValue; + +/** + * Uninitialized value implementation. + * + * @author Matt Coley + */ +public class UninitializedValueImpl implements UninitializedValue { + public static final UninitializedValue UNINITIALIZED_VALUE = new UninitializedValueImpl(); + + private UninitializedValueImpl() {} + + @Override + public String toString() { + return "."; + } +} From cb8a6855434336a571206a32b84c052a3a647100 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 10 Nov 2024 11:54:44 -0500 Subject: [PATCH 5/8] Automatically bump compiler target version when using expression compiler In *most* cases we probably don't care too much if the input version is Java 6 and we emit Java 8 classes. We're only interested in the bytecode of our expression, and its rare that such version bumps will affect the output. I mean, its better than just failing too... --- .../services/assembler/ExpressionCompiler.java | 15 ++++++++++++++- .../assembler/ExpressionCompilerTest.java | 10 ++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/ExpressionCompiler.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/ExpressionCompiler.java index 422a2713d..acdbb6fd4 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/assembler/ExpressionCompiler.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/ExpressionCompiler.java @@ -137,6 +137,19 @@ else if (!isSafeName(name)) methodVariables = method.getLocalVariables(); } + /** + * Set the target version of Java. + *
+ * Java 8 would pass 8. + * + * @param versionTarget + * Java version target. + * Range of supported values: [{@link JavacCompiler#getMinTargetVersion()} - {@link JavaVersion#get()}] + */ + public void setVersionTarget(int versionTarget) { + this.versionTarget = versionTarget; + } + /** * Compiles the given expression with the current context. * @@ -159,7 +172,7 @@ public ExpressionResult compile(@Nonnull String expression) { } // Compile the generated class - JavacArguments arguments = new JavacArguments(className, code, null, versionTarget, -1, true, false, false); + JavacArguments arguments = new JavacArguments(className, code, null, Math.max(versionTarget, JavacCompiler.getMinTargetVersion()), -1, true, false, false); CompilerResult result = javac.compile(arguments, workspace, null); if (!result.wasSuccess()) { Throwable exception = result.getException(); diff --git a/recaf-core/src/test/java/software/coley/recaf/services/assembler/ExpressionCompilerTest.java b/recaf-core/src/test/java/software/coley/recaf/services/assembler/ExpressionCompilerTest.java index 732cce4dd..c1c8afa3d 100644 --- a/recaf-core/src/test/java/software/coley/recaf/services/assembler/ExpressionCompilerTest.java +++ b/recaf-core/src/test/java/software/coley/recaf/services/assembler/ExpressionCompilerTest.java @@ -142,6 +142,16 @@ void classAndMethodContextForStaticInitializer() { assertSuccess(result); } + @Test + void ignoreTooOldTargetVersion() { + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); + assembler.setVersionTarget(1); + ExpressionResult result = compile(assembler, """ + System.out.println("We do not compile against Java 1"); + """); + assertSuccess(result); + } + @Test void ignoreNonExistingTypeForFields() { ClassWriter cw = new ClassWriter(0); From 3252f682df89e5644e28c53a95bba2a779274f24 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 10 Nov 2024 15:22:04 -0500 Subject: [PATCH 6/8] Support inner classes in the expression compiler --- .../assembler/ExpressionCompiler.java | 574 +----------------- .../compile/stub/ClassStubGenerator.java | 534 ++++++++++++++++ .../ExpressionHostingClassStubGenerator.java | 350 +++++++++++ .../compile/stub/InnerClassStubGenerator.java | 94 +++ .../assembler/ExpressionCompilerTest.java | 17 + .../test/dummy/ClassWithInnerAndMembers.java | 36 ++ 6 files changed, 1055 insertions(+), 550 deletions(-) create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/compile/stub/ClassStubGenerator.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/compile/stub/ExpressionHostingClassStubGenerator.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/compile/stub/InnerClassStubGenerator.java create mode 100644 recaf-core/src/testFixtures/java/software/coley/recaf/test/dummy/ClassWithInnerAndMembers.java diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/ExpressionCompiler.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/ExpressionCompiler.java index acdbb6fd4..d251107e1 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/assembler/ExpressionCompiler.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/ExpressionCompiler.java @@ -1,9 +1,8 @@ package software.coley.recaf.services.assembler; +import dev.xdark.blw.type.MethodType; import dev.xdark.blw.type.Types; -import dev.xdark.blw.type.*; import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; import jakarta.enterprise.context.Dependent; import jakarta.inject.Inject; import me.darknet.assembler.printer.JvmClassPrinter; @@ -11,26 +10,29 @@ import me.darknet.assembler.printer.PrintContext; import org.objectweb.asm.Opcodes; import org.slf4j.Logger; -import regexodus.Matcher; import regexodus.Pattern; import software.coley.recaf.analytics.logging.Logging; +import software.coley.recaf.info.InnerClassInfo; import software.coley.recaf.info.JvmClassInfo; -import software.coley.recaf.info.member.BasicLocalVariable; import software.coley.recaf.info.member.FieldMember; import software.coley.recaf.info.member.LocalVariable; import software.coley.recaf.info.member.MethodMember; -import software.coley.recaf.path.ClassPathNode; import software.coley.recaf.services.compile.CompilerDiagnostic; import software.coley.recaf.services.compile.CompilerResult; import software.coley.recaf.services.compile.JavacArguments; import software.coley.recaf.services.compile.JavacCompiler; -import software.coley.recaf.util.*; +import software.coley.recaf.services.compile.stub.ExpressionHostingClassStubGenerator; +import software.coley.recaf.util.AccessFlag; +import software.coley.recaf.util.JavaVersion; +import software.coley.recaf.util.NumberUtil; +import software.coley.recaf.util.RegexUtil; +import software.coley.recaf.util.StringUtil; import software.coley.recaf.workspace.model.Workspace; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.util.*; -import java.util.stream.Collectors; +import java.util.Collections; +import java.util.List; /** * Compiles Java source expressions into JASM. @@ -41,7 +43,7 @@ public class ExpressionCompiler { private static final Logger logger = Logging.get(ExpressionCompiler.class); private static final Pattern IMPORT_EXTRACT_PATTERN = RegexUtil.pattern("^\\s*(import \\w.+;)"); - private static final String EXPR_MARKER = "/* EXPR_START */"; + public static final String EXPR_MARKER = "/* EXPR_START */"; private final JavacCompiler javac; private final Workspace workspace; private final AssemblerPipelineGeneralConfig assemblerConfig; @@ -52,6 +54,7 @@ public class ExpressionCompiler { private int versionTarget; private List fields; private List methods; + private List innerClasses; private String methodName; private MethodType methodType; private int methodFlags; @@ -77,6 +80,7 @@ public void clearContext() { versionTarget = JavaVersion.get(); fields = Collections.emptyList(); methods = Collections.emptyList(); + innerClasses = Collections.emptyList(); methodName = "generated"; methodType = Types.methodType("()V"); methodFlags = Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE; // Bridge used to denote default state. @@ -93,19 +97,18 @@ public void clearContext() { public void setClassContext(@Nonnull JvmClassInfo classInfo) { String type = classInfo.getName(); String superType = classInfo.getSuperName(); - className = isSafeInternalClassName(type) ? type : "obfuscated_class"; + className = type; classAccess = classInfo.getAccess(); versionTarget = NumberUtil.intClamp(classInfo.getVersion() - JvmClassInfo.BASE_VERSION, JavacCompiler.getMinTargetVersion(), JavaVersion.get()); - superName = superType != null && isSafeInternalClassName(superType) ? superType : null; - implementing = classInfo.getInterfaces().stream() - .filter(ExpressionCompiler::isSafeInternalClassName) - .toList(); + superName = classInfo.getSuperName(); + implementing = classInfo.getInterfaces(); fields = classInfo.getFields(); methods = classInfo.getMethods(); + innerClasses = classInfo.getInnerClasses(); // We use bridge to denote that the default flags are set. // When we assign a class, there may be non-static fields/methods the user will want to interact with. - // Thus, we should clear our flags from the default so they can do that. + // Thus, we should clear our flags from the default so that they can do that. if (AccessFlag.isBridge(methodFlags)) methodFlags = 0; @@ -122,16 +125,7 @@ public void setClassContext(@Nonnull JvmClassInfo classInfo) { * Method to pull info from. */ public void setMethodContext(@Nonnull MethodMember method) { - // Map edge cases for disallowed names. - String name = method.getName(); - if (name.equals("")) - name = "instance_ctor"; - else if (name.equals("")) - name = "static_ctor"; - else if (!isSafeName(name)) - name = "obfuscated_method"; - - methodName = name; + methodName = method.getName(); methodType = Types.methodType(method.getDescriptor()); methodFlags = method.getAccess(); methodVariables = method.getLocalVariables(); @@ -164,9 +158,12 @@ public void setVersionTarget(int versionTarget) { @Nonnull public ExpressionResult compile(@Nonnull String expression) { // Generate source of a class to house the expression within + ExpressionHostingClassStubGenerator stubber; String code; try { - code = generateClass(expression); + stubber = new ExpressionHostingClassStubGenerator(workspace, classAccess, className, superName, implementing, + fields, methods, innerClasses, methodFlags, methodName, methodType, methodVariables, expression); + code = stubber.generate(); } catch (ExpressionCompileException ex) { return new ExpressionResult(ex); } @@ -190,7 +187,7 @@ public ExpressionResult compile(@Nonnull String expression) { try { PrintContext context = new PrintContext<>(assemblerConfig.getDisassemblyIndent().getValue()); JvmClassPrinter printer = new JvmClassPrinter(new ByteArrayInputStream(klass)); - JvmMethodPrinter method = (JvmMethodPrinter) printer.method(methodName, methodDescriptorWithVariables()); + JvmMethodPrinter method = (JvmMethodPrinter) printer.method(stubber.getAdaptedMethodName(), stubber.methodDescriptorWithVariables()); if (method == null) return new ExpressionResult(new ExpressionCompileException("Target method was not in generated class")); method.setLabelPrefix("g"); @@ -203,387 +200,6 @@ public ExpressionResult compile(@Nonnull String expression) { } } - /** - * @param expression - * Expression to compile. - * - * @return Generated class to pass to {@code javac} for full-class compilation. - * - * @throws ExpressionCompileException - * When the class could not be fully generated. - */ - @Nonnull - private String generateClass(@Nonnull String expression) throws ExpressionCompileException { - StringBuilder code = new StringBuilder(); - - // Append package - if (className.indexOf('/') > 0) { - String packageName = className.replace('/', '.').substring(0, className.lastIndexOf('/')); - code.append("package ").append(packageName).append(";\n"); - } - - // Add imports from the user defined expression. - // Remove the imports from the expression once copied to the output code. - StringBuilder expressionBuffer = new StringBuilder(); - expression.lines().forEach(l -> { - Matcher matcher = IMPORT_EXTRACT_PATTERN.matcher(l); - if (matcher.find()) { - code.append(matcher.group(1)).append('\n'); - } else { - expressionBuffer.append(l).append('\n'); - } - }); - expression = expressionBuffer.toString(); - - // Class structure - boolean isEnum = AccessFlag.isEnum(classAccess); - code.append(isEnum ? "enum " : "abstract class ").append(StringUtil.shortenPath(className)); - if (superName != null && !superName.equals("java/lang/Object") && !superName.equals("java/lang/Enum")) - code.append(" extends ").append(superName.replace('/', '.')); - if (implementing != null && !implementing.isEmpty()) - code.append(" implements ").append(implementing.stream().map(s -> s.replace('/', '.')).collect(Collectors.joining(", "))).append(' '); - code.append("{\n"); - - // Enum constants must come first if the class is an enum. - if (isEnum) { - int enumConsts = 0; - for (FieldMember field : fields) { - if (field.getDescriptor().length() == 1) - continue; - InstanceType fieldDesc = Types.instanceTypeFromDescriptor(field.getDescriptor()); - if (fieldDesc.internalName().equals(className) && field.hasFinalModifier() && field.hasStaticModifier()) { - if (enumConsts > 0) - code.append(", "); - code.append(field.getName()); - enumConsts++; - } - } - code.append(';'); - } - - // Need to build the method structure to house the expression. - // We'll start off with the access level. - int parameterVarIndex = 0; - if (AccessFlag.isPublic(methodFlags)) - code.append("public "); - else if (AccessFlag.isProtected(methodFlags)) - code.append("protected "); - else if (AccessFlag.isPrivate(methodFlags)) - code.append("private "); - if (AccessFlag.isStatic(methodFlags)) - code.append("static "); - else - parameterVarIndex++; - - // Add the return type. - ClassType returnType = methodType.returnType(); - if (returnType instanceof PrimitiveType primitiveReturn) { - code.append(primitiveReturn.name()).append(' '); - } else if (returnType instanceof InstanceType instanceType) { - code.append(instanceType.internalName().replace('/', '.')).append(' '); - } else if (returnType instanceof ArrayType arrayReturn) { - ClassType componentReturnType = arrayReturn.componentType(); - if (componentReturnType instanceof PrimitiveType primitiveReturn) { - code.append(primitiveReturn.name()); - } else if (componentReturnType instanceof InstanceType instanceType) { - code.append(instanceType.internalName().replace('/', '.')); - } - code.append("[]".repeat(arrayReturn.dimensions())); - } - - // Now the method name. - code.append(' ').append(methodName).append('('); - - // And now the parameters. - int parameterCount = methodType.parameterTypes().size(); - Set usedVariables = new HashSet<>(); - for (int i = 0; i < parameterCount; i++) { - // Lookup the parameter variable - LocalVariable parameterVariable = getParameterVariable(parameterVarIndex, i); - String parameterName = parameterVariable.getName(); - - // Record the parameter as being used - usedVariables.add(parameterName); - - // Skip if the parameter is illegally named. - if (!isSafeName(parameterName)) - continue; - - // Skip parameters with types that aren't accessible in the workspace. - String descriptor = parameterVariable.getDescriptor(); - if (isMissingType(descriptor)) - continue; - - // Append the parameter. - NameType varInfo = getInfo(parameterName, descriptor); - parameterVarIndex += varInfo.size; - code.append(varInfo.className).append(' ').append(varInfo.name); - if (i < parameterCount - 1) code.append(", "); - } - for (LocalVariable variable : methodVariables) { - String name = variable.getName(); - - // Skip illegal named variables and the implicit 'this' - if (!isSafeName(name) || name.equals("this")) - continue; - - // Skip if we already included the parameter in the loop above. - boolean hasPriorParameters = !usedVariables.isEmpty(); - if (!usedVariables.add(name)) - continue; - - // Skip parameters with types that aren't accessible in the workspace. - String descriptor = variable.getDescriptor(); - if (isMissingType(descriptor)) - continue; - - // Append the parameter. - NameType varInfo = getInfo(name, descriptor); - if (hasPriorParameters) - code.append(", "); - code.append(varInfo.className).append(' ').append(varInfo.name); - } - - // If we skipped the last parameter for some reason we need to remove the trailing ', ' before closing - // off the parameters section. - if (code.substring(code.length() - 2).endsWith(", ")) - code.setLength(code.length() - 2); - - // Close off declaration and add a throws so the user doesn't need to specify try-catch. - code.append(") throws Throwable { " + EXPR_MARKER + " \n"); - code.append(expression); - code.append("}\n"); - - // Stub out fields / methods - for (FieldMember field : fields) { - // Skip stubbing compiler-generated fields. - if (field.hasBridgeModifier() || field.hasSyntheticModifier()) - continue; - - // Skip stubbing of illegally named fields. - String name = field.getName(); - if (!isSafeName(name)) - continue; - NameType fieldNameType = getInfo(name, field.getDescriptor()); - if (!isSafeClassName(fieldNameType.className)) - continue; - - // Skip enum constants, we added those earlier. - if (fieldNameType.className.equals(className.replace('/', '.')) && field.hasFinalModifier() && field.hasStaticModifier()) - continue; - - // Skip fields with types that aren't accessible in the workspace. - if (isMissingType(field.getDescriptor())) continue; - - // Append the field. The only modifier that we care about here is if it is static or not. - if (field.hasStaticModifier()) - code.append("static "); - code.append(fieldNameType.className).append(' ').append(fieldNameType.name).append(";\n"); - } - for (MethodMember method : methods) { - // Skip stubbing compiler-generated methods. - if (method.hasBridgeModifier() || method.hasSyntheticModifier()) - continue; - - // Skip stubbing of illegally named methods. - String name = method.getName(); - boolean isCtor = false; - if (name.equals("")) { - if (isEnum) // Skip constructors for enum classes since we always drop enum const parameters. - continue; - isCtor = true; - } else if (!isSafeName(name)) - continue; - - // Skip stubbing the method if it is the one we're assembling the expression within. - String descriptor = method.getDescriptor(); - MethodType localMethodType = Types.methodType(descriptor); - if (methodName.equals(name) && methodType.equals(localMethodType)) - continue; - - // Skip enum's 'valueOf' - if (isEnum && - name.equals("valueOf") && - descriptor.equals("(Ljava/lang/String;)L" + className + ";")) - continue; - - // Skip stubbing of methods with bad return types / bad parameter types. - NameType returnInfo = getInfo(name, localMethodType.returnType().descriptor()); - if (!isSafeClassName(returnInfo.className)) - continue; - List parameterTypes = localMethodType.parameterTypes(); - if (!parameterTypes.stream().map(p -> { - try { - return getInfo("p", p.descriptor()).className(); - } catch (Throwable t) { - return "\0"; // Bogus which will throw off the safe name check. - } - }).allMatch(ExpressionCompiler::isSafeClassName)) - continue; - - // Skip methods with return/parameter types that aren't accessible in the workspace. - boolean hasMissingType = false; - Type[] types = new Type[parameterTypes.size() + 1]; - for (int i = 0; i < types.length - 1; i++) - types[i] = parameterTypes.get(i); - types[parameterTypes.size()] = localMethodType.returnType(); - for (Type type : types) { - hasMissingType = isMissingType(type); - if (hasMissingType) - break; - } - if (hasMissingType) continue; - - // Stub the method. Start with the access modifiers. - if (method.hasPublicModifier()) - code.append("public "); - else if (method.hasProtectedModifier()) - code.append("protected "); - else if (method.hasPrivateModifier()) - code.append("private "); - if (method.hasStaticModifier()) - code.append("static "); - - // Method name. Consider edge case for constructors. - if (isCtor) - code.append(StringUtil.shortenPath(className)).append('('); - else - code.append(returnInfo.className).append(' ').append(returnInfo.name).append('('); - - // Add the parameters. We only care about the types, names don't really matter. - List methodParameterTypes = parameterTypes; - parameterCount = methodParameterTypes.size(); - for (int i = 0; i < parameterCount; i++) { - ClassType paramType = methodParameterTypes.get(i); - NameType paramInfo = getInfo("p" + i, paramType.descriptor()); - code.append(paramInfo.className).append(' ').append(paramInfo.name); - if (i < parameterCount - 1) code.append(", "); - } - code.append(") { "); - if (isCtor) { - // If we know the parent type, we need to properly implement the constructor. - // If we don't know the parent type, we cannot generate a valid constructor. - ClassPathNode superPath = superName == null ? null : workspace.findJvmClass(superName); - if (superPath == null && superName != null) - throw new ExpressionCompileException("Cannot generate 'super(...)' for constructor, " + - "missing type information for: " + superName); - if (superPath != null) { - // To make it easy, we'll find the simplest constructor in the parent class and pass dummy values. - // Unlike regular methods we cannot just say 'throw new RuntimeException();' since calling - // the 'super(...)' is required. - MethodType parentConstructor = superPath.getValue().methodStream() - .filter(m -> m.getName().equals("")) - .map(m -> Types.methodType(m.getDescriptor())) - .min(Comparator.comparingInt(a -> a.parameterTypes().size())) - .orElse(null); - if (parentConstructor != null) { - code.append("super("); - parameterCount = parentConstructor.parameterTypes().size(); - for (int i = 0; i < parameterCount; i++) { - ClassType type = parentConstructor.parameterTypes().get(i); - if (type instanceof ObjectType) { - code.append("null"); - } else { - char prim = type.descriptor().charAt(0); - if (prim == 'Z') - code.append("false"); - else - code.append('0'); - } - if (i < parameterCount - 1) code.append(", "); - } - code.append(");"); - } - } - } else { - code.append("throw new RuntimeException();"); - } - code.append(" }\n"); - } - - // Done with the class - code.append("}\n"); - return code.toString(); - } - - /** - * @param name - * Variable name. - * @param descriptor - * Variable descriptor. - * - * @return Variable info wrapper. - * - * @throws ExpressionCompileException - * When the variable descriptor is malformed. - */ - @Nonnull - private NameType getInfo(@Nonnull String name, @Nonnull String descriptor) throws ExpressionCompileException { - int size; - String className; - if (Types.isPrimitive(descriptor)) { - PrimitiveType primitiveType = Types.primitiveFromDesc(descriptor); - size = Types.category(primitiveType); - className = primitiveType.name(); - } else if (descriptor.charAt(0) == '[') { - ArrayType arrayParameterType = Types.arrayTypeFromDescriptor(descriptor); - ClassType componentReturnType = arrayParameterType.componentType(); - if (componentReturnType instanceof PrimitiveType primitiveParameter) { - className = primitiveParameter.name(); - } else if (componentReturnType instanceof InstanceType instanceType) { - className = instanceType.internalName().replace('/', '.').replace('$', '.'); - } else { - throw new ExpressionCompileException("Illegal component type: " + componentReturnType); - } - className += "[]".repeat(arrayParameterType.dimensions()); - size = 1; - } else { - size = 1; - className = Types.instanceTypeFromDescriptor(descriptor).internalName().replace('/', '.').replace('$', '.'); - } - return new NameType(size, name, className); - } - - /** - * @param index - * Local variable index. - * - * @return Variable entry from the target method, or {@code null} if not known. - */ - @Nullable - private LocalVariable findVar(int index) { - if (methodVariables == null) return null; - return methodVariables.stream() - .filter(l -> l.getIndex() == index) - .findFirst().orElse(null); - } - - /** - * @param parameterVarIndex - * Local variable index of the parameter. - * @param parameterIndex - * Parameter index. - * - * @return Local variable info of the parameter. - */ - @Nonnull - private LocalVariable getParameterVariable(int parameterVarIndex, int parameterIndex) { - LocalVariable parameterVariable = findVar(parameterVarIndex); - if (parameterVariable == null) { - List parameterTypes = methodType.parameterTypes(); - ClassType parameterType; - if (parameterIndex < parameterTypes.size()) { - parameterType = parameterTypes.get(parameterIndex); - } else { - logger.warn("Could not resolve parameter variable (pVar={}, pIndex={}) in {}", parameterVarIndex, parameterIndex, methodName); - parameterType = Types.OBJECT; - } - parameterVariable = new BasicLocalVariable(parameterVarIndex, "p" + parameterIndex, parameterType.descriptor(), null); - - } - return parameterVariable; - } - /** * @param code * Generateed code to work with. @@ -610,146 +226,4 @@ private static List remap(@Nonnull String code, @Nonnull Lis .map(d -> d.withLine(d.line() - lineOffset)) .toList(); } - - /** - * Note: The logic for appending parameters to the desc within this method must align with {@link #generateClass(String)}. - * - * @return The method descriptor with additional parameters from the {@link #methodVariables} appended at the end. - * - * @throws ExpressionCompileException - * When parameter variable information cannot be found. - */ - @Nonnull - private String methodDescriptorWithVariables() throws ExpressionCompileException { - StringBuilder sb = new StringBuilder("("); - int parameterVarIndex = AccessFlag.isStatic(methodFlags) ? 0 : 1; - int parameterCount = methodType.parameterTypes().size(); - Set usedVariables = new HashSet<>(); - for (int i = 0; i < parameterCount; i++) { - LocalVariable parameterVariable = getParameterVariable(parameterVarIndex, i); - String parameterName = parameterVariable.getName(); - usedVariables.add(parameterName); - if (!isSafeName(parameterName)) - continue; - String descriptor = parameterVariable.getDescriptor(); - if (isMissingType(descriptor)) - continue; - NameType varInfo = getInfo(parameterName, descriptor); - parameterVarIndex += varInfo.size; - sb.append(descriptor); - } - for (LocalVariable variable : methodVariables) { - String name = variable.getName(); - if (!isSafeName(name) || name.equals("this")) - continue; - if (!usedVariables.add(name)) - continue; - String descriptor = variable.getDescriptor(); - if (isMissingType(descriptor)) - continue; - sb.append(descriptor); - } - sb.append(')').append(methodType.returnType().descriptor()); - return sb.toString(); - } - - /** - * @param descriptor - * Some non-method descriptor. - * - * @return {@code true} if the type in the descriptor is found in the {@link #workspace}. - */ - private boolean isMissingType(@Nonnull String descriptor) { - Type type = Types.typeFromDescriptor(descriptor); - return isMissingType(type); - } - - /** - * @param type - * Some non-method type. - * - * @return {@code true} if the type in the descriptor is found in the {@link #workspace}. - */ - private boolean isMissingType(@Nonnull Type type) { - if (type instanceof InstanceType instanceType && workspace.findClass(instanceType.internalName()) == null) - return true; - else - return type instanceof ArrayType arrayType - && arrayType.rootComponentType() instanceof InstanceType instanceType - && workspace.findClass(instanceType.internalName()) == null; - } - - /** - * @param name - * Name to check. - * - * @return {@code true} when it can be used as a variable name safely. - */ - private static boolean isSafeName(@Nonnull String name) { - // Name must not be empty. - if (name.isEmpty()) - return false; - - // Must be comprised of valid identifier characters. - char first = name.charAt(0); - if (!Character.isJavaIdentifierStart(first)) - return false; - char[] chars = name.toCharArray(); - for (int i = 1; i < chars.length; i++) { - if (!Character.isJavaIdentifierPart(chars[i])) - return false; - } - - // Cannot be a reserved keyword. - return !Keywords.getKeywords().contains(name); - } - - /** - * @param internalName - * Name to check. Expected to be in the internal format. IE {@code java/lang/String}. - * - * @return {@code true} when it can be used as a class name safely. - */ - private static boolean isSafeInternalClassName(@Nonnull String internalName) { - // Sanity check input - if (internalName.indexOf('.') >= 0) - throw new IllegalStateException("Saw source name format, expected internal name format"); - - // All package name portions and the class name must be valid names. - return StringUtil.fastSplit(internalName, true, '/').stream() - .allMatch(ExpressionCompiler::isSafeName); - } - - /** - * @param name - * Name to check. Expected to be in the source format. IE {@code java.lang.String}. - * - * @return {@code true} when it can be used as a class name safely. - */ - private static boolean isSafeClassName(@Nonnull String name) { - // Sanity check input - if (name.indexOf('/') >= 0) - throw new IllegalStateException("Saw internal name format, expected source name format"); - - // Allow primitives - if (software.coley.recaf.util.Types.isPrimitiveClassName(name)) - return true; - - // All package name portions and the class name must be valid names. - return StringUtil.fastSplit(name, true, '.').stream() - .allMatch(ExpressionCompiler::isSafeName); - } - - /** - * Wrapper for field/variable info. - * - * @param size - * Variable slot size. - * @param name - * Variable name. - * @param className - * Variable class type name. - */ - private record NameType(int size, @Nonnull String name, @Nonnull String className) { - } } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/compile/stub/ClassStubGenerator.java b/recaf-core/src/main/java/software/coley/recaf/services/compile/stub/ClassStubGenerator.java new file mode 100644 index 000000000..9133caf29 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/compile/stub/ClassStubGenerator.java @@ -0,0 +1,534 @@ +package software.coley.recaf.services.compile.stub; + +import dev.xdark.blw.type.ArrayType; +import dev.xdark.blw.type.ClassType; +import dev.xdark.blw.type.InstanceType; +import dev.xdark.blw.type.MethodType; +import dev.xdark.blw.type.ObjectType; +import dev.xdark.blw.type.PrimitiveType; +import dev.xdark.blw.type.Type; +import dev.xdark.blw.type.Types; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import software.coley.recaf.info.ClassInfo; +import software.coley.recaf.info.InnerClassInfo; +import software.coley.recaf.info.member.FieldMember; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.services.assembler.ExpressionCompileException; +import software.coley.recaf.util.AccessFlag; +import software.coley.recaf.util.Keywords; +import software.coley.recaf.util.StringUtil; +import software.coley.recaf.workspace.model.Workspace; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Base stub generator for classes. + * + * @author Matt Coley + */ +public abstract class ClassStubGenerator { + protected final Workspace workspace; + protected final int classAccess; + protected final String className; + protected final String superName; + protected final List implementing; + protected final List fields; + protected final List methods; + protected final List innerClasses; + + /** + * @param workspace + * Workspace to pull class information from. + * @param classAccess + * Host class access modifiers. + * @param className + * Host class name. + * @param superName + * Host class super name. + * @param implementing + * Host class interfaces implemented. + * @param fields + * Host class declared fields. + * @param methods + * Host class declared methods. + * @param innerClasses + * Host class declared inner classes. + */ + public ClassStubGenerator(@Nonnull Workspace workspace, + int classAccess, + @Nonnull String className, + @Nullable String superName, + @Nonnull List implementing, + @Nonnull List fields, + @Nonnull List methods, + @Nonnull List innerClasses) { + this.workspace = workspace; + this.classAccess = classAccess; + this.className = isSafeInternalClassName(className) ? className : "obfuscated_class"; + this.superName = superName != null && isSafeInternalClassName(superName) ? superName : null; + this.implementing = implementing.stream() + .filter(ClassStubGenerator::isSafeInternalClassName) + .toList(); + this.fields = fields; + this.methods = methods; + this.innerClasses = innerClasses; + } + + /** + * @return Generated stub for the target class. + * + * @throws ExpressionCompileException + * When the class could not be fully stubbed out. + */ + public abstract String generate() throws ExpressionCompileException; + + /** + * Appends a package declaration if the {@link #className} is not in the default package. + * + * @param code + * Class code to append package declaration to. + */ + protected void appendPackage(@Nonnull StringBuilder code) { + if (className.indexOf('/') > 0) { + String packageName = className.replace('/', '.').substring(0, className.lastIndexOf('/')); + code.append("package ").append(packageName).append(";\n"); + } + } + + /** + * Appends the class's access modifiers, type (class, interface, enum), name, extended type, and any implemented interfaces. + * + * @param code + * Class code to append the class type structure to. + */ + protected void appendClassStructure(@Nonnull StringBuilder code) { + // Class structure + code.append(AccessFlag.isEnum(classAccess) ? "enum " : getLocalModifier() + " class ").append(getLocalName()); + if (superName != null && !superName.equals("java/lang/Object") && !superName.equals("java/lang/Enum")) + code.append(" extends ").append(superName.replace('/', '.')); + if (implementing != null && !implementing.isEmpty()) + code.append(" implements ").append(implementing.stream().map(s -> s.replace('/', '.')).collect(Collectors.joining(", "))).append(' '); + code.append("{\n"); + } + + /** + * Appends enum constants defined in {@link #fields} to the class. + * Must be called before {@link #appendFields(StringBuilder)}. + * + * @param code + * Class code to append enum constants to. + */ + protected void appendEnumConsts(@Nonnull StringBuilder code) { + // Enum constants must come first if the class is an enum. + if (AccessFlag.isEnum(classAccess)) { + int enumConsts = 0; + for (FieldMember field : fields) { + if (field.getDescriptor().length() == 1) + continue; + InstanceType fieldDesc = Types.instanceTypeFromDescriptor(field.getDescriptor()); + if (fieldDesc.internalName().equals(className) && field.hasFinalModifier() && field.hasStaticModifier()) { + if (enumConsts > 0) + code.append(", "); + code.append(field.getName()); + enumConsts++; + } + } + code.append(';'); + } + } + + /** + * Appends all non-enum constant fields to the class. + * + * @param code + * Class code to append the fields to. + * + * @throws ExpressionCompileException + * When the fields could not be stubbed out. + */ + protected void appendFields(@Nonnull StringBuilder code) throws ExpressionCompileException { + // Stub out fields / methods + for (FieldMember field : fields) { + // Skip stubbing compiler-generated fields. + if (field.hasBridgeModifier() || field.hasSyntheticModifier()) + continue; + + // Skip stubbing of illegally named fields. + String name = field.getName(); + if (!isSafeName(name)) + continue; + NameType fieldNameType = getInfo(name, field.getDescriptor()); + if (!isSafeClassName(fieldNameType.className)) + continue; + + // Skip enum constants, we added those earlier. + if (AccessFlag.isEnum(classAccess) + && fieldNameType.className.equals(className.replace('/', '.')) + && field.hasFinalModifier() + && field.hasStaticModifier()) + continue; + + // Skip fields with types that aren't accessible in the workspace. + if (isMissingType(field.getDescriptor())) continue; + + // Append the field. The only modifier that we care about here is if it is static or not. + if (field.hasStaticModifier()) + code.append("static "); + code.append(fieldNameType.className).append(' ').append(fieldNameType.name).append(";\n"); + } + } + + /** + * Appends all method stubs to the class. + * Some methods can be skipped by implementing {@link #doSkipMethod(String, MethodType)}. + * + * @param code + * Class code to append the methods to. + * + * @throws ExpressionCompileException + * When the methods could not be stubbed out. + */ + protected void appendMethods(@Nonnull StringBuilder code) throws ExpressionCompileException { + boolean isEnum = AccessFlag.isEnum(classAccess); + for (MethodMember method : methods) { + // Skip stubbing compiler-generated methods. + if (method.hasBridgeModifier() || method.hasSyntheticModifier()) + continue; + + // Skip stubbing of illegally named methods. + String name = method.getName(); + boolean isCtor = false; + if (name.equals("")) { + // Skip constructors for enum classes since we always drop enum const parameters. + if (isEnum) + continue; + isCtor = true; + } else if (!isSafeName(name)) + continue; + + // Skip stubbing the method if it is the one we're assembling the expression within. + String descriptor = method.getDescriptor(); + MethodType localMethodType = Types.methodType(descriptor); + if (doSkipMethod(name, localMethodType)) + continue; + + // Skip enum's 'valueOf' + if (isEnum && + name.equals("valueOf") && + descriptor.equals("(Ljava/lang/String;)L" + className + ";")) + continue; + + // Skip stubbing of methods with bad return types / bad parameter types. + NameType returnInfo = getInfo(name, localMethodType.returnType().descriptor()); + if (!isSafeClassName(returnInfo.className)) + continue; + List parameterTypes = localMethodType.parameterTypes(); + if (!parameterTypes.stream().map(p -> { + try { + return getInfo("p", p.descriptor()).className(); + } catch (Throwable t) { + return "\0"; // Bogus which will throw off the safe name check. + } + }).allMatch(ClassStubGenerator::isSafeClassName)) + continue; + + // Skip methods with return/parameter types that aren't accessible in the workspace. + boolean hasMissingType = false; + Type[] types = new Type[parameterTypes.size() + 1]; + for (int i = 0; i < types.length - 1; i++) + types[i] = parameterTypes.get(i); + types[parameterTypes.size()] = localMethodType.returnType(); + for (Type type : types) { + hasMissingType = isMissingType(type); + if (hasMissingType) + break; + } + if (hasMissingType) continue; + + // Stub the method. Start with the access modifiers. + if (method.hasPublicModifier()) + code.append("public "); + else if (method.hasProtectedModifier()) + code.append("protected "); + else if (method.hasPrivateModifier()) + code.append("private "); + if (method.hasStaticModifier()) + code.append("static "); + + // Method name. Consider edge case for constructors. + if (isCtor) + code.append(getLocalName()).append('('); + else + code.append(returnInfo.className()).append(' ').append(returnInfo.name).append('('); + + // Add the parameters. We only care about the types, names don't really matter. + List methodParameterTypes = parameterTypes; + int parameterCount = methodParameterTypes.size(); + for (int i = 0; i < parameterCount; i++) { + ClassType paramType = methodParameterTypes.get(i); + + // Skip this parameter if it is an inner class's outer "this" reference + if (isCtor + && paramType instanceof ObjectType paramObjectType + && className.startsWith(paramObjectType.internalName() + '$')) + continue; + + NameType paramInfo = getInfo("p" + i, paramType.descriptor()); + code.append(paramInfo.className).append(' ').append(paramInfo.name); + if (i < parameterCount - 1) code.append(", "); + } + code.append(") { "); + if (isCtor) { + // If we know the parent type, we need to properly implement the constructor. + // If we don't know the parent type, we cannot generate a valid constructor. + ClassPathNode superPath = superName == null ? null : workspace.findJvmClass(superName); + if (superPath == null && superName != null) + throw new ExpressionCompileException("Cannot generate 'super(...)' for constructor, " + + "missing type information for: " + superName); + if (superPath != null) { + // To make it easy, we'll find the simplest constructor in the parent class and pass dummy values. + // Unlike regular methods we cannot just say 'throw new RuntimeException();' since calling + // the 'super(...)' is required. + MethodType parentConstructor = superPath.getValue().methodStream() + .filter(m -> m.getName().equals("")) + .map(m -> Types.methodType(m.getDescriptor())) + .min(Comparator.comparingInt(a -> a.parameterTypes().size())) + .orElse(null); + if (parentConstructor != null) { + code.append("super("); + parameterCount = parentConstructor.parameterTypes().size(); + for (int i = 0; i < parameterCount; i++) { + ClassType type = parentConstructor.parameterTypes().get(i); + if (type instanceof ObjectType) { + code.append("null"); + } else { + char prim = type.descriptor().charAt(0); + if (prim == 'Z') + code.append("false"); + else + code.append('0'); + } + if (i < parameterCount - 1) code.append(", "); + } + code.append(");"); + } + } + } else { + code.append("throw new RuntimeException();"); + } + code.append(" }\n"); + } + } + + + /** + * @param code + * Class code to append the inner classes to. + * + * @throws ExpressionCompileException + * When the inner classes could not be stubbed out. + */ + protected void appendInnerClasses(@Nonnull StringBuilder code) throws ExpressionCompileException { + for (InnerClassInfo innerClass : innerClasses) { + String innerClassName = innerClass.getInnerClassName(); + if (innerClassName.length() <= className.length()) + continue; + ClassPathNode innerClassPath = workspace.findClass(innerClassName); + if (innerClassPath != null) { + ClassInfo innerClassInfo = innerClassPath.getValue(); + ClassStubGenerator generator = new InnerClassStubGenerator(workspace, + innerClassInfo.getAccess(), + innerClassInfo.getName(), + innerClassInfo.getSuperName(), + innerClassInfo.getInterfaces(), + innerClassInfo.getFields(), + innerClassInfo.getMethods(), + innerClassInfo.getInnerClasses() + ); + String inner = generator.generate(); + code.append('\n').append(inner).append('\n'); + } + } + } + + /** + * Ends the class definition. + * + * @param code + * Class code to append end to. + */ + protected void appendClassEnd(@Nonnull StringBuilder code) { + // Done with the class + code.append("}\n"); + } + + /** + * Controls which methods are included in {@link #appendMethods(StringBuilder)}. + * + * @param name + * Method name. + * @param type + * Method type. + * + * @return {@code true} to skip. {@code false} to include in output stubbing. + */ + protected abstract boolean doSkipMethod(@Nonnull String name, @Nonnull MethodType type); + + /** + * @return Modifier to prefix {@code Foo} in {@code class Foo {}}. + */ + @Nonnull + public String getLocalModifier() { + return "abstract"; + } + + /** + * @return Name string to where {@code Foo} is in {@code class Foo {}}. + */ + @Nonnull + protected String getLocalName() { + return StringUtil.shortenPath(className); + } + + /** + * @param descriptor + * Some non-method descriptor. + * + * @return {@code true} if the type in the descriptor is found in the {@link #workspace}. + */ + protected boolean isMissingType(@Nonnull String descriptor) { + Type type = Types.typeFromDescriptor(descriptor); + return isMissingType(type); + } + + /** + * @param type + * Some non-method type. + * + * @return {@code true} if the type in the descriptor is found in the {@link #workspace}. + */ + protected boolean isMissingType(@Nonnull Type type) { + if (type instanceof InstanceType instanceType && workspace.findClass(instanceType.internalName()) == null) + return true; + else + return type instanceof ArrayType arrayType + && arrayType.rootComponentType() instanceof InstanceType instanceType + && workspace.findClass(instanceType.internalName()) == null; + } + + /** + * @param name + * Name to check. + * + * @return {@code true} when it can be used as a variable name safely. + */ + protected static boolean isSafeName(@Nonnull String name) { + // Name must not be empty. + if (name.isEmpty()) + return false; + + // Must be comprised of valid identifier characters. + char first = name.charAt(0); + if (!Character.isJavaIdentifierStart(first)) + return false; + char[] chars = name.toCharArray(); + for (int i = 1; i < chars.length; i++) { + if (!Character.isJavaIdentifierPart(chars[i])) + return false; + } + + // Cannot be a reserved keyword. + return !Keywords.getKeywords().contains(name); + } + + /** + * @param internalName + * Name to check. Expected to be in the internal format. IE {@code java/lang/String}. + * + * @return {@code true} when it can be used as a class name safely. + */ + protected static boolean isSafeInternalClassName(@Nonnull String internalName) { + // Sanity check input + if (internalName.indexOf('.') >= 0) + throw new IllegalStateException("Saw source name format, expected internal name format"); + + // All package name portions and the class name must be valid names. + return StringUtil.fastSplit(internalName, true, '/').stream() + .allMatch(ClassStubGenerator::isSafeName); + } + + /** + * @param name + * Name to check. Expected to be in the source format. IE {@code java.lang.String}. + * + * @return {@code true} when it can be used as a class name safely. + */ + protected static boolean isSafeClassName(@Nonnull String name) { + // Sanity check input + if (name.indexOf('/') >= 0) + throw new IllegalStateException("Saw internal name format, expected source name format"); + + // Allow primitives + if (software.coley.recaf.util.Types.isPrimitiveClassName(name)) + return true; + + // All package name portions and the class name must be valid names. + return StringUtil.fastSplit(name, true, '.').stream() + .allMatch(ClassStubGenerator::isSafeName); + } + + /** + * @param name + * Variable name. + * @param descriptor + * Variable descriptor. + * + * @return Variable info wrapper. + * + * @throws ExpressionCompileException + * When the variable descriptor is malformed. + */ + @Nonnull + protected static NameType getInfo(@Nonnull String name, @Nonnull String descriptor) throws ExpressionCompileException { + int size; + String className; + if (Types.isPrimitive(descriptor)) { + PrimitiveType primitiveType = Types.primitiveFromDesc(descriptor); + size = Types.category(primitiveType); + className = primitiveType.name(); + } else if (descriptor.charAt(0) == '[') { + ArrayType arrayParameterType = Types.arrayTypeFromDescriptor(descriptor); + ClassType componentReturnType = arrayParameterType.componentType(); + if (componentReturnType instanceof PrimitiveType primitiveParameter) { + className = primitiveParameter.name(); + } else if (componentReturnType instanceof InstanceType instanceType) { + className = instanceType.internalName().replace('/', '.').replace('$', '.'); + } else { + throw new ExpressionCompileException("Illegal component type: " + componentReturnType); + } + className += "[]".repeat(arrayParameterType.dimensions()); + size = 1; + } else { + size = 1; + className = Types.instanceTypeFromDescriptor(descriptor).internalName().replace('/', '.').replace('$', '.'); + } + return new NameType(size, name, className); + } + + /** + * Wrapper for field/variable info. + * + * @param size + * Variable slot size. + * @param name + * Variable name. + * @param className + * Variable class type name. + */ + protected record NameType(int size, @Nonnull String name, @Nonnull String className) { + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/compile/stub/ExpressionHostingClassStubGenerator.java b/recaf-core/src/main/java/software/coley/recaf/services/compile/stub/ExpressionHostingClassStubGenerator.java new file mode 100644 index 000000000..c4def4041 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/compile/stub/ExpressionHostingClassStubGenerator.java @@ -0,0 +1,350 @@ +package software.coley.recaf.services.compile.stub; + +import dev.xdark.blw.type.ArrayType; +import dev.xdark.blw.type.ClassType; +import dev.xdark.blw.type.InstanceType; +import dev.xdark.blw.type.MethodType; +import dev.xdark.blw.type.PrimitiveType; +import dev.xdark.blw.type.Types; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.slf4j.Logger; +import regexodus.Matcher; +import regexodus.Pattern; +import software.coley.recaf.analytics.logging.Logging; +import software.coley.recaf.info.InnerClassInfo; +import software.coley.recaf.info.member.BasicLocalVariable; +import software.coley.recaf.info.member.FieldMember; +import software.coley.recaf.info.member.LocalVariable; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.services.assembler.ExpressionCompileException; +import software.coley.recaf.services.assembler.ExpressionCompiler; +import software.coley.recaf.util.AccessFlag; +import software.coley.recaf.util.RegexUtil; +import software.coley.recaf.workspace.model.Workspace; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class stub generator which implements a specific method with a user-defined expression. + * + * @author Matt Coley + * @see ExpressionCompiler#compile(String) + */ +public class ExpressionHostingClassStubGenerator extends ClassStubGenerator { + private static final Logger logger = Logging.get(ExpressionHostingClassStubGenerator.class); + private static final Pattern IMPORT_EXTRACT_PATTERN = RegexUtil.pattern("^\\s*(import \\w.+;)"); + private final int methodFlags; + private final String methodName; + private final MethodType methodType; + private final List methodVariables; + private final String expression; + + /** + * @param workspace + * Workspace to pull class information from. + * @param classAccess + * Host class access modifiers. + * @param className + * Host class name. + * @param superName + * Host class super name. + * @param implementing + * Host class interfaces implemented. + * @param fields + * Host class declared fields. + * @param methods + * Host class declared methods. + * @param innerClasses + * Host class declared inner classes. + * @param methodFlags + * Expression hosting method's access modifiers. + * @param methodName + * Expression hosting method's name. + * @param methodType + * Expression hosting method arguments + return type. + * @param methodVariables + * Expression hosting method's local variables. + * @param expression + * The expression to insert into the target hosting method. + */ + public ExpressionHostingClassStubGenerator(@Nonnull Workspace workspace, + int classAccess, + @Nonnull String className, + @Nullable String superName, + @Nonnull List implementing, + @Nonnull List fields, + @Nonnull List methods, + @Nonnull List innerClasses, + int methodFlags, + @Nonnull String methodName, + @Nonnull MethodType methodType, + @Nonnull List methodVariables, + @Nonnull String expression) { + super(workspace, classAccess, className, superName, implementing, fields, methods, innerClasses); + + // Map edge cases for disallowed names. + if (methodName.equals("")) + methodName = "instance_ctor"; + else if (methodName.equals("")) + methodName = "static_ctor"; + else if (!isSafeName(methodName)) + methodName = "obfuscated_method"; + + // Assign expression host method details + this.methodFlags = methodFlags; + this.methodName = methodName; + this.methodType = methodType; + this.methodVariables = methodVariables; + this.expression = expression; + } + + @Override + public String generate() throws ExpressionCompileException { + String localExpression = expression; + + StringBuilder code = new StringBuilder(); + appendPackage(code); + localExpression = appendExpressionImports(code, localExpression); + appendClassStructure(code); + appendEnumConsts(code); + appendExpressionMethod(code, localExpression); + appendFields(code); + appendMethods(code); + appendInnerClasses(code); + appendClassEnd(code); + + return code.toString(); + } + + @Override + protected boolean doSkipMethod(@Nonnull String name, @Nonnull MethodType type) { + // We want to skip generating a stub of the method our expression will reside within. + return methodName.equals(name) && methodType.equals(type); + } + + /** + * @return Adapted method name for compiler-safe use. + */ + @Nonnull + public String getAdaptedMethodName() { + return methodName; + } + + /** + * Expressions can contain imports at the top so that the end-user can work without needing fully qualified names. + * We want to take those out and append them to the class we're generating, and update the expression to remove + * the imports so that we can slap it into the method body later without syntax issues coming from imports being + * used in a method body. + * + * @param code + * Class code to append imports to. + * @param expression + * Expression to extract imports from. + * + * @return Modified expression (without imports) + */ + @Nonnull + private String appendExpressionImports(@Nonnull StringBuilder code, @Nonnull String expression) { + // Add imports from the user defined expression. + // Remove the imports from the expression once copied to the output code. + StringBuilder expressionBuffer = new StringBuilder(); + expression.lines().forEach(l -> { + Matcher matcher = IMPORT_EXTRACT_PATTERN.matcher(l); + if (matcher.find()) { + code.append(matcher.group(1)).append('\n'); + } else { + expressionBuffer.append(l).append('\n'); + } + }); + return expressionBuffer.toString(); + } + + /** + * @param code + * Class code to append method definition to. + * @param expression + * User-defined expression. + * + * @throws ExpressionCompileException + * When the expression hosting method could not be fully generated. + */ + private void appendExpressionMethod(@Nonnull StringBuilder code, @Nonnull String expression) throws ExpressionCompileException { + // Need to build the method structure to house the expression. + // We'll start off with the access level. + int parameterVarIndex = 0; + if (AccessFlag.isPublic(methodFlags)) + code.append("public "); + else if (AccessFlag.isProtected(methodFlags)) + code.append("protected "); + else if (AccessFlag.isPrivate(methodFlags)) + code.append("private "); + if (AccessFlag.isStatic(methodFlags)) + code.append("static "); + else + parameterVarIndex++; + + // Add the return type. + ClassType returnType = methodType.returnType(); + if (returnType instanceof PrimitiveType primitiveReturn) { + code.append(primitiveReturn.name()).append(' '); + } else if (returnType instanceof InstanceType instanceType) { + code.append(instanceType.internalName().replace('/', '.')).append(' '); + } else if (returnType instanceof ArrayType arrayReturn) { + ClassType componentReturnType = arrayReturn.componentType(); + if (componentReturnType instanceof PrimitiveType primitiveReturn) { + code.append(primitiveReturn.name()); + } else if (componentReturnType instanceof InstanceType instanceType) { + code.append(instanceType.internalName().replace('/', '.')); + } + code.append("[]".repeat(arrayReturn.dimensions())); + } + + // Now the method name. + code.append(' ').append(methodName).append('('); + + // And now the parameters. + int parameterCount = methodType.parameterTypes().size(); + Set usedVariables = new HashSet<>(); + for (int i = 0; i < parameterCount; i++) { + // Lookup the parameter variable + LocalVariable parameterVariable = getParameterVariable(parameterVarIndex, i); + String parameterName = parameterVariable.getName(); + + // Record the parameter as being used + usedVariables.add(parameterName); + + // Skip if the parameter is illegally named. + if (!isSafeName(parameterName)) + continue; + + // Skip parameters with types that aren't accessible in the workspace. + String descriptor = parameterVariable.getDescriptor(); + if (isMissingType(descriptor)) + continue; + + // Append the parameter. + NameType varInfo = getInfo(parameterName, descriptor); + parameterVarIndex += varInfo.size(); + code.append(varInfo.className()).append(' ').append(varInfo.name()); + if (i < parameterCount - 1) code.append(", "); + } + for (LocalVariable variable : methodVariables) { + String name = variable.getName(); + + // Skip illegal named variables and the implicit 'this' + if (!isSafeName(name) || name.equals("this")) + continue; + + // Skip if we already included the parameter in the loop above. + boolean hasPriorParameters = !usedVariables.isEmpty(); + if (!usedVariables.add(name)) + continue; + + // Skip parameters with types that aren't accessible in the workspace. + String descriptor = variable.getDescriptor(); + if (isMissingType(descriptor)) + continue; + + // Append the parameter. + NameType varInfo = getInfo(name, descriptor); + if (hasPriorParameters) + code.append(", "); + code.append(varInfo.className()).append(' ').append(varInfo.name()); + } + + // If we skipped the last parameter for some reason we need to remove the trailing ', ' before closing + // off the parameters section. + if (code.substring(code.length() - 2).endsWith(", ")) + code.setLength(code.length() - 2); + + // Close off declaration and add a throws so the user doesn't need to specify try-catch. + code.append(") throws Throwable { " + ExpressionCompiler.EXPR_MARKER + " \n"); + code.append(expression); + code.append("}\n"); + } + + /** + * Note: The logic for appending parameters to the desc within this method must align with {@link #generate()}. + * + * @return The method descriptor with additional parameters from the {@link #methodVariables} appended at the end. + * + * @throws ExpressionCompileException + * When parameter variable information cannot be found. + */ + @Nonnull + public String methodDescriptorWithVariables() throws ExpressionCompileException { + StringBuilder sb = new StringBuilder("("); + int parameterVarIndex = AccessFlag.isStatic(methodFlags) ? 0 : 1; + int parameterCount = methodType.parameterTypes().size(); + Set usedVariables = new HashSet<>(); + for (int i = 0; i < parameterCount; i++) { + LocalVariable parameterVariable = getParameterVariable(parameterVarIndex, i); + String parameterName = parameterVariable.getName(); + usedVariables.add(parameterName); + if (!isSafeName(parameterName)) + continue; + String descriptor = parameterVariable.getDescriptor(); + if (isMissingType(descriptor)) + continue; + NameType varInfo = getInfo(parameterName, descriptor); + parameterVarIndex += varInfo.size(); + sb.append(descriptor); + } + for (LocalVariable variable : methodVariables) { + String name = variable.getName(); + if (!isSafeName(name) || name.equals("this")) + continue; + if (!usedVariables.add(name)) + continue; + String descriptor = variable.getDescriptor(); + if (isMissingType(descriptor)) + continue; + sb.append(descriptor); + } + sb.append(')').append(methodType.returnType().descriptor()); + return sb.toString(); + } + + /** + * @param index + * Local variable index. + * + * @return Variable entry from the target method, or {@code null} if not known. + */ + @Nullable + private LocalVariable findVar(int index) { + if (methodVariables == null) return null; + return methodVariables.stream() + .filter(l -> l.getIndex() == index) + .findFirst().orElse(null); + } + + /** + * @param parameterVarIndex + * Local variable index of the parameter. + * @param parameterIndex + * Parameter index. + * + * @return Local variable info of the parameter. + */ + @Nonnull + private LocalVariable getParameterVariable(int parameterVarIndex, int parameterIndex) { + LocalVariable parameterVariable = findVar(parameterVarIndex); + if (parameterVariable == null) { + List parameterTypes = methodType.parameterTypes(); + ClassType parameterType; + if (parameterIndex < parameterTypes.size()) { + parameterType = parameterTypes.get(parameterIndex); + } else { + logger.warn("Could not resolve parameter variable (pVar={}, pIndex={}) in {}", parameterVarIndex, parameterIndex, methodName); + parameterType = Types.OBJECT; + } + parameterVariable = new BasicLocalVariable(parameterVarIndex, "p" + parameterIndex, parameterType.descriptor(), null); + + } + return parameterVariable; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/compile/stub/InnerClassStubGenerator.java b/recaf-core/src/main/java/software/coley/recaf/services/compile/stub/InnerClassStubGenerator.java new file mode 100644 index 000000000..be3bee5d8 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/compile/stub/InnerClassStubGenerator.java @@ -0,0 +1,94 @@ +package software.coley.recaf.services.compile.stub; + +import dev.xdark.blw.type.MethodType; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import software.coley.recaf.info.InnerClassInfo; +import software.coley.recaf.info.member.FieldMember; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.services.assembler.ExpressionCompileException; +import software.coley.recaf.util.AccessFlag; +import software.coley.recaf.workspace.model.Workspace; + +import java.util.List; + +/** + * Class stub generator which emits classes under the assumption they are inner classes of an outer class. + * + * @author Matt Coley + */ +public class InnerClassStubGenerator extends ClassStubGenerator { + /** + * @param workspace + * Workspace to pull class information from. + * @param classAccess + * Host class access modifiers. + * @param className + * Host class name. + * @param superName + * Host class super name. + * @param implementing + * Host class interfaces implemented. + * @param fields + * Host class declared fields. + * @param methods + * Host class declared methods. + * @param innerClasses + * Host class declared inner classes. + */ + public InnerClassStubGenerator(@Nonnull Workspace workspace, + int classAccess, + @Nonnull String className, + @Nullable String superName, + @Nonnull List implementing, + @Nonnull List fields, + @Nonnull List methods, + @Nonnull List innerClasses) { + super(workspace, classAccess, className, superName, implementing, fields, methods, innerClasses); + } + + @Nonnull + @Override + protected String getLocalName() { + // Will be "OuterClass$TheInner" + String localName = super.getLocalName(); + + // We just want "TheInner" + int innerSplit = localName.indexOf('$'); + if (innerSplit > 0) + localName = localName.substring(innerSplit + 1); + + return localName; + } + + @Nonnull + @Override + public String getLocalModifier() { + if (AccessFlag.isAbstract(classAccess)) + return "abstract"; + + // If the inner class (this context) is not abstract, we do not want to force + // it to be abstract in order to allow expressions to do "new Inner()" and stuff. + return ""; + } + + @Override + public String generate() throws ExpressionCompileException { + StringBuilder code = new StringBuilder(); + + appendClassStructure(code); + appendEnumConsts(code); + appendFields(code); + appendMethods(code); + appendInnerClasses(code); + appendClassEnd(code); + + return code.toString(); + } + + @Override + protected boolean doSkipMethod(@Nonnull String name, @Nonnull MethodType type) { + // Do not skip any methods + return false; + } +} diff --git a/recaf-core/src/test/java/software/coley/recaf/services/assembler/ExpressionCompilerTest.java b/recaf-core/src/test/java/software/coley/recaf/services/assembler/ExpressionCompilerTest.java index c1c8afa3d..19b4c2432 100644 --- a/recaf-core/src/test/java/software/coley/recaf/services/assembler/ExpressionCompilerTest.java +++ b/recaf-core/src/test/java/software/coley/recaf/services/assembler/ExpressionCompilerTest.java @@ -15,6 +15,7 @@ import software.coley.recaf.test.TestBase; import software.coley.recaf.test.TestClassUtils; import software.coley.recaf.test.dummy.ClassWithFieldsAndMethods; +import software.coley.recaf.test.dummy.ClassWithInnerAndMembers; import software.coley.recaf.test.dummy.ClassWithRequiredConstructor; import software.coley.recaf.test.dummy.DummyEnum; import software.coley.recaf.workspace.model.Workspace; @@ -33,12 +34,14 @@ class ExpressionCompilerTest extends TestBase { static JvmClassInfo targetClass; static JvmClassInfo targetCtorClass; static JvmClassInfo targetEnum; + static JvmClassInfo targetOuterWithInner; @BeforeAll static void setup() throws IOException { targetClass = TestClassUtils.fromRuntimeClass(ClassWithFieldsAndMethods.class); targetCtorClass = TestClassUtils.fromRuntimeClass(ClassWithRequiredConstructor.class); targetEnum = TestClassUtils.fromRuntimeClass(DummyEnum.class); + targetOuterWithInner = TestClassUtils.fromRuntimeClass(ClassWithInnerAndMembers.class); workspace = TestClassUtils.fromBundle(TestClassUtils.fromClasses(targetClass, targetCtorClass, targetEnum)); workspaceManager.setCurrent(workspace); } @@ -142,6 +145,20 @@ void classAndMethodContextForStaticInitializer() { assertSuccess(result); } + @Test + void classWithInnerReferences() { + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); + assembler.setClassContext(targetOuterWithInner); + ExpressionResult result = compile(assembler, """ + TheInner inner = new TheInner(); + System.out.println("foo: " + foo); + System.out.println("bar: " + inner.bar); + inner.strings.add("something"); + inner.innerToOuter(); + """); + assertSuccess(result); + } + @Test void ignoreTooOldTargetVersion() { ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); diff --git a/recaf-core/src/testFixtures/java/software/coley/recaf/test/dummy/ClassWithInnerAndMembers.java b/recaf-core/src/testFixtures/java/software/coley/recaf/test/dummy/ClassWithInnerAndMembers.java new file mode 100644 index 000000000..a92c33b69 --- /dev/null +++ b/recaf-core/src/testFixtures/java/software/coley/recaf/test/dummy/ClassWithInnerAndMembers.java @@ -0,0 +1,36 @@ +package software.coley.recaf.test.dummy; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class to test basic inner-outer relation, for a regular inner class with fields. + */ +@SuppressWarnings("all") +public class ClassWithInnerAndMembers { + private int foo = 10; + + void outer() { + System.out.println(foo); + } + + void outerToInner(TheInner inner) { + inner.inner(); + System.out.println(inner.bar); + System.out.println(inner.strings.getFirst()); + } + + public class TheInner { + private final List strings = new ArrayList<>(); + private int bar = 10; + + void inner() { + System.out.println(foo); + System.out.println(bar); + } + + void innerToOuter() { + outer(); + } + } +} From e8d01089dc64359f2fb28afde252505cff46f065 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 10 Nov 2024 19:43:18 -0500 Subject: [PATCH 7/8] Fix transformer allocations being cached even when dependent scoped --- .../recaf/services/transform/TransformationManager.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/recaf-core/src/main/java/software/coley/recaf/services/transform/TransformationManager.java b/recaf-core/src/main/java/software/coley/recaf/services/transform/TransformationManager.java index 80bb79843..ccc834892 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/transform/TransformationManager.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/transform/TransformationManager.java @@ -8,12 +8,14 @@ import jakarta.inject.Inject; import org.slf4j.Logger; import software.coley.collections.Unchecked; +import software.coley.recaf.Bootstrap; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.services.Service; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -43,7 +45,12 @@ public TransformationManager(@Nonnull TransformationManagerConfig config, @Nonnu for (Instance.Handle handle : jvmTransformers.handles()) { Bean bean = handle.getBean(); Class transformerClass = Unchecked.cast(bean.getBeanClass()); - jvmTransformerSuppliers.put(transformerClass, handle::get); + jvmTransformerSuppliers.put(transformerClass, () -> { + // Even though our transformers may be @Dependent scoped, we need to do a new lookup each time we want + // a new instance to get our desired scope behavior. If we re-use the instance handle that is injected + // here then even @Dependent scoped beans will yield the same instance again and again. + return Bootstrap.get().get(transformerClass); + }); } } From 40ecd657bc52670789f59d7c5fe266fa5940c6ef Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 10 Nov 2024 21:15:00 -0500 Subject: [PATCH 8/8] Fix the workspace analysis modal getting stuck Dumb fix is just to wait 100ms so the 1ms to show the modal isn't raced by the analysis (which can also just be 1ms) --- .../software/coley/recaf/ui/pane/WorkspaceInformationPane.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceInformationPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceInformationPane.java index 3eccefc00..022a02b10 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceInformationPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceInformationPane.java @@ -107,7 +107,7 @@ public WorkspaceInformationPane(@Nonnull TextProviderService textService, // When the summary is done, clear the "loading..." overlay. CompletableFuture.allOf(summaryFutures.toArray(CompletableFuture[]::new)) - .whenCompleteAsync((ignored, error) -> modal.hide(true), FxThreadUtil.executor()); + .whenCompleteAsync((ignored, error) -> FxThreadUtil.delayedRun(100, () -> modal.hide(true))); } @Nonnull