From 2d67d9f755aef281d2c83459495f795a1114ac8d Mon Sep 17 00:00:00 2001 From: Justus Garbe Date: Fri, 15 Sep 2023 22:54:05 +0200 Subject: [PATCH 1/5] Add jasm as depend --- dependencies.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dependencies.gradle b/dependencies.gradle index 0ca39c286..a8797d648 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,5 +1,6 @@ def asmVersion = '9.5' def cafeDudeVersion = '1.10.2' +def jasmVersion = '2.0.0' def junitVersion = '5.9.2' project.ext { @@ -38,6 +39,8 @@ project.ext { instrument_server = 'software.coley:instrumentation-server:1.3.6' + jasm = "com.github.Nowilltolife:jasm:$jasmVersion" + jackson = 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.15.2' jlinker = 'com.github.xxDark:jlinker:1.0.7' From e12e6debda6ad60a99d78a93f4daf3248c2bfeaf Mon Sep 17 00:00:00 2001 From: Justus Garbe Date: Thu, 21 Sep 2023 10:42:18 +0200 Subject: [PATCH 2/5] Basic jasm implementation --- dependencies.gradle | 7 +- recaf-core/build.gradle | 2 + .../coley/recaf/config/ConfigGroups.java | 4 + .../assembler/AbstractAssemblerPipeline.java | 101 ++++++++++++ .../assembler/AndroidAssemblerPipeline.java | 15 ++ .../services/assembler/AssemblerPipeline.java | 63 ++++++++ .../assembler/AssemblerPipelineConfig.java | 6 + .../AssemblerPipelineGeneralConfig.java | 27 ++++ .../assembler/AssemblerPipelineManager.java | 58 +++++++ .../assembler/JvmAssemblerPipeline.java | 101 ++++++++++++ .../services/assembler/PipelineContainer.java | 14 ++ .../BasicClassContextMenuProviderFactory.java | 8 +- .../BasicFieldContextMenuProviderFactory.java | 5 + ...BasicMethodContextMenuProviderFactory.java | 5 + .../FileTypeAssociationServiceConfig.java | 1 + .../recaf/services/navigation/Actions.java | 61 +++++++- .../coley/recaf/ui/LanguageStylesheets.java | 12 ++ .../richtext/syntax/RegexLanguages.java | 12 ++ .../syntax/RegexSyntaxHighlighter.java | 4 +- .../pane/editing/assembler/AssemblerPane.java | 147 ++++++++++++++++++ recaf-ui/src/main/resources/syntax/jasm.css | 15 ++ recaf-ui/src/main/resources/syntax/jasm.json | 39 +++++ 22 files changed, 700 insertions(+), 7 deletions(-) create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/assembler/AbstractAssemblerPipeline.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/assembler/AndroidAssemblerPipeline.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipeline.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineConfig.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineGeneralConfig.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineManager.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/assembler/JvmAssemblerPipeline.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/assembler/PipelineContainer.java create mode 100644 recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java create mode 100644 recaf-ui/src/main/resources/syntax/jasm.css create mode 100644 recaf-ui/src/main/resources/syntax/jasm.json diff --git a/dependencies.gradle b/dependencies.gradle index a8797d648..90a94340c 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,6 +1,6 @@ def asmVersion = '9.5' def cafeDudeVersion = '1.10.2' -def jasmVersion = '2.0.0' +def jasmVersion = '8663cec34f' def junitVersion = '5.9.2' project.ext { @@ -39,7 +39,10 @@ project.ext { instrument_server = 'software.coley:instrumentation-server:1.3.6' - jasm = "com.github.Nowilltolife:jasm:$jasmVersion" + //jasm_core = "com.github.Nowilltolife.Jasm:jasm-core:$jasmVersion" + //jasm_composistion_jvm = "com.github.Nowilltolife.Jasm:jasm-composition-jvm:$jasmVersion" + jasm_core = "me.darknet:jasm-core:2.0.0" + jasm_composistion_jvm = "me.darknet:jasm-composition-jvm:2.0.0" jackson = 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.15.2' diff --git a/recaf-core/build.gradle b/recaf-core/build.gradle index 4eda31f3e..3964987eb 100644 --- a/recaf-core/build.gradle +++ b/recaf-core/build.gradle @@ -40,6 +40,8 @@ dependencies { api jlinker api openrewrite api regex + api jasm_core + api jasm_composistion_jvm } // Force generation of gversion data class when the version information is not up-to-date diff --git a/recaf-core/src/main/java/software/coley/recaf/config/ConfigGroups.java b/recaf-core/src/main/java/software/coley/recaf/config/ConfigGroups.java index 767743ff3..2f63c8e89 100644 --- a/recaf-core/src/main/java/software/coley/recaf/config/ConfigGroups.java +++ b/recaf-core/src/main/java/software/coley/recaf/config/ConfigGroups.java @@ -20,6 +20,10 @@ public final class ConfigGroups { * Group for analyzing components. */ public static final String SERVICE_ANALYSIS = SERVICE + PACKAGE_SPLIT + "analysis"; + /** + * Group for assembler components. + */ + public static final String SERVICE_ASSEMBLER = SERVICE + PACKAGE_SPLIT + "assembler"; /** * Group for compiler components. */ diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AbstractAssemblerPipeline.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AbstractAssemblerPipeline.java new file mode 100644 index 000000000..49bba7ac8 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AbstractAssemblerPipeline.java @@ -0,0 +1,101 @@ +package software.coley.recaf.services.assembler; + +import com.google.common.reflect.ClassPath; +import me.darknet.assembler.compiler.ClassRepresentation; +import me.darknet.assembler.error.Error; +import me.darknet.assembler.error.Result; +import me.darknet.assembler.printer.*; +import software.coley.recaf.info.ClassInfo; +import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.info.annotation.Annotated; +import software.coley.recaf.info.annotation.AnnotationInfo; +import software.coley.recaf.info.member.ClassMember; +import software.coley.recaf.path.AnnotationPathNode; +import software.coley.recaf.path.ClassMemberPathNode; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.PathNode; + +public abstract class AbstractAssemblerPipeline + implements AssemblerPipeline { + + protected final PrintContext context; + protected final AssemblerPipelineConfig pipelineConfig; + + public AbstractAssemblerPipeline(AssemblerPipelineGeneralConfig config, AssemblerPipelineConfig pipelineConfig) { + this.context = new PrintContext<>(config.getDisassemblyIndent().getValue()); + this.pipelineConfig = pipelineConfig; + + config.getDisassemblyIndent().addChangeListener((observable, oldVal, newVal) -> context.setIndentStep(newVal)); + } + + protected abstract Result classPrinter(ClassPathNode location); + + protected Result memberPrinter(ClassMemberPathNode node) { + ClassPathNode owner = node.getParent(); + if(owner == null) + return Result.err(Error.of("Dangling member", null)); + + ClassMember member = node.getValue(); + return classPrinter(owner).flatMap((printer) -> { + Printer memberPrinter = null; + + if(member.isMethod()) { + memberPrinter = printer.method(member.getName(), member.getDescriptor()); + } else if(member.isField()) { + memberPrinter = printer.field(member.getName(), member.getDescriptor()); + } + + if(memberPrinter == null) { + return Result.err(Error.of("Failed to find member", null)); + } else { + return Result.ok(memberPrinter); + } + }); + } + + protected Result annotationPrinter(AnnotationPathNode node) { + if(node.getParent() == null) { + return Result.err(Error.of("Dangling annotation", null)); + } + + Object parent = node.getParent().getValue(); + Result parentPrinter; + if(parent instanceof ClassPathNode classNode) { + parentPrinter = classPrinter(classNode); + } else if(parent instanceof ClassMemberPathNode classMember) { + parentPrinter = memberPrinter(classMember); + } else { + return Result.err(Error.of("Invalid parent type", null)); + } + + AnnotationInfo annotation = node.getValue(); + + if(parent instanceof Annotated annotated) { + + return parentPrinter.flatMap((printer) -> { + if (printer instanceof AnnotationHolder holder) { + return Result.ok(holder.annotation(annotated.getAnnotations().indexOf(annotation))); + } else { + return Result.err(Error.of("Parent is not an annotation holder", null)); + } + }); + + } else { + return Result.err(Error.of("Parent cannot hold annotations", null)); + } + } + + protected String print(Printer printer) { + context.clear(); + printer.print(context); + return context.toString(); + } + + @Override + public AssemblerPipelineConfig getConfig() { + return pipelineConfig; + } + + + +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AndroidAssemblerPipeline.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AndroidAssemblerPipeline.java new file mode 100644 index 000000000..d9aeead03 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AndroidAssemblerPipeline.java @@ -0,0 +1,15 @@ +package software.coley.recaf.services.assembler; + +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class AndroidAssemblerPipeline { + + @Inject + public AndroidAssemblerPipeline(@Nonnull AssemblerPipelineGeneralConfig config) { + // TODO: Implement when dalvik assembler pipeline is implemented + } + +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipeline.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipeline.java new file mode 100644 index 000000000..1a465874c --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipeline.java @@ -0,0 +1,63 @@ +package software.coley.recaf.services.assembler; + +import me.darknet.assembler.ast.ASTElement; +import me.darknet.assembler.compiler.ClassRepresentation; +import me.darknet.assembler.error.Error; +import me.darknet.assembler.error.Result; +import me.darknet.assembler.parser.DeclarationParser; +import me.darknet.assembler.parser.ParsingResult; +import me.darknet.assembler.parser.Token; +import me.darknet.assembler.parser.Tokenizer; +import software.coley.recaf.info.ClassInfo; +import software.coley.recaf.info.annotation.Annotated; +import software.coley.recaf.info.annotation.AnnotationInfo; +import software.coley.recaf.info.member.ClassMember; +import software.coley.recaf.info.member.FieldMember; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.path.AnnotationPathNode; +import software.coley.recaf.path.ClassMemberPathNode; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.PathNode; + +import java.util.List; + +public interface AssemblerPipeline { + + default List tokenize(String input, String source) { + return new Tokenizer().tokenize(source, input); + } + + default ParsingResult> roughParse(List tokens) { + return new DeclarationParser().parseDeclarations(tokens); + } + + Result> concreteParse(List elements); + + Result> pipe(List tokens, C info); + + Result assemble(List elements, C info); + + Result disassemble(ClassPathNode node); + + Result disassemble(ClassMemberPathNode node); + + Result disassemble(AnnotationPathNode node); + + default Result disassemble(PathNode node) { + if (node instanceof ClassPathNode classPathNode) + return disassemble(classPathNode); + if (node instanceof ClassMemberPathNode classMemberPathNode) + return disassemble(classMemberPathNode); + if (node instanceof AnnotationPathNode annotationPathNode) + return disassemble(annotationPathNode); + return Result.err(Error.of("Unsupported node type: " + node.getClass().getName(), null)); + } + + R getRepresentation(C info); + + C getClassInfo(R representation); + + AssemblerPipelineConfig getConfig(); + + +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineConfig.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineConfig.java new file mode 100644 index 000000000..1f650465f --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineConfig.java @@ -0,0 +1,6 @@ +package software.coley.recaf.services.assembler; + +import software.coley.recaf.config.ConfigContainer; + +public interface AssemblerPipelineConfig extends ConfigContainer { +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineGeneralConfig.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineGeneralConfig.java new file mode 100644 index 000000000..0f8e0e523 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineGeneralConfig.java @@ -0,0 +1,27 @@ +package software.coley.recaf.services.assembler; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import software.coley.observables.ObservableString; +import software.coley.recaf.config.BasicConfigContainer; +import software.coley.recaf.config.BasicConfigValue; +import software.coley.recaf.config.ConfigGroups; +import software.coley.recaf.services.ServiceConfig; + +@ApplicationScoped +public class AssemblerPipelineGeneralConfig extends BasicConfigContainer implements ServiceConfig { + + private final ObservableString disassemblyIndent = new ObservableString(" "); + + @Inject + public AssemblerPipelineGeneralConfig() { + super(ConfigGroups.SERVICE_ASSEMBLER, AssemblerPipelineManager.SERVICE_ID + ConfigGroups.PACKAGE_SPLIT + + "general" + CONFIG_SUFFIX); + + addValue(new BasicConfigValue<>("disassembly_indent", String.class, disassemblyIndent)); + } + + public ObservableString getDisassemblyIndent() { + return disassemblyIndent; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineManager.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineManager.java new file mode 100644 index 000000000..df80e45f2 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineManager.java @@ -0,0 +1,58 @@ +package software.coley.recaf.services.assembler; + +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import me.darknet.assembler.compiler.ClassRepresentation; +import software.coley.recaf.info.ClassInfo; +import software.coley.recaf.path.PathNode; +import software.coley.recaf.services.Service; +import software.coley.recaf.services.ServiceConfig; + +@ApplicationScoped +public class AssemblerPipelineManager implements Service { + public static final String SERVICE_ID = "assembler_pipeline"; + private final AssemblerPipelineGeneralConfig config; + private final JvmAssemblerPipeline jvmAssemblerPipeline; + private final AndroidAssemblerPipeline androidAssemblerPipeline; + + @Inject + public AssemblerPipelineManager(@Nonnull AssemblerPipelineGeneralConfig config, + @Nonnull JvmAssemblerPipeline jvmAssemblerPipeline, + @Nonnull AndroidAssemblerPipeline androidAssemblerPipeline) { + this.config = config; + this.jvmAssemblerPipeline = jvmAssemblerPipeline; + this.androidAssemblerPipeline = androidAssemblerPipeline; + } + + @Nonnull + @Override + public String getServiceId() { + return SERVICE_ID; + } + + @Nonnull + @Override + public ServiceConfig getServiceConfig() { + return config; + } + + public AssemblerPipeline getPipeline(PathNode node) { + ClassInfo info = node.getValueOfType(ClassInfo.class); + if(info == null) + throw new IllegalStateException("Failed to find class info for node: " + node); + if(info.isJvmClass()) { + return jvmAssemblerPipeline; + } else { + // TODO: Implement when dalvik assembler pipeline is implemented + throw new UnsupportedOperationException("Dalvik assembler pipeline is not implemented"); + } + } + + public JvmAssemblerPipeline getJvmAssemblerPipeline() { + return jvmAssemblerPipeline; + } + public AndroidAssemblerPipeline getAndroidAssemblerPipeline() { + return androidAssemblerPipeline; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/JvmAssemblerPipeline.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/JvmAssemblerPipeline.java new file mode 100644 index 000000000..7149cd981 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/JvmAssemblerPipeline.java @@ -0,0 +1,101 @@ +package software.coley.recaf.services.assembler; + +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import me.darknet.assembler.ast.ASTElement; +import me.darknet.assembler.compile.JavaClassRepresentation; +import me.darknet.assembler.compile.JvmCompiler; +import me.darknet.assembler.error.Error; +import me.darknet.assembler.error.Result; +import me.darknet.assembler.parser.BytecodeFormat; +import me.darknet.assembler.parser.Token; +import me.darknet.assembler.parser.processor.ASTProcessor; +import me.darknet.assembler.printer.ClassPrinter; +import me.darknet.assembler.printer.JvmClassPrinter; +import me.darknet.assembler.printer.MemberPrinter; +import me.darknet.assembler.printer.Printer; +import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.info.annotation.Annotated; +import software.coley.recaf.info.annotation.AnnotationInfo; +import software.coley.recaf.info.builder.JvmClassInfoBuilder; +import software.coley.recaf.info.member.ClassMember; +import software.coley.recaf.path.AnnotationPathNode; +import software.coley.recaf.path.ClassMemberPathNode; +import software.coley.recaf.path.ClassPathNode; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; + +@ApplicationScoped +public class JvmAssemblerPipeline extends AbstractAssemblerPipeline { + + private final ASTProcessor processor = new ASTProcessor(BytecodeFormat.JVM); + private final String name = "JVM"; + + @Inject + public JvmAssemblerPipeline(@Nonnull AssemblerPipelineGeneralConfig config) { + super(config, null); + } + + @Override + public Result> concreteParse(List elements) { + return processor.processAST(elements); + } + + @Override + public Result> pipe(List tokens, JvmClassInfo info) { + var result = roughParse(tokens); + if(result.isOk()) { + return concreteParse(result.get()); + } else { + return result; + } + } + + @Override + public Result assemble(List elements, JvmClassInfo info) { + JvmCompiler compiler = new JvmCompiler(); + return null; + } + + @Override + public Result disassemble(ClassPathNode node) { + return classPrinter(node).map(this::print); + } + + @Override + public Result disassemble(ClassMemberPathNode node) { + return memberPrinter(node).map(this::print); + } + + @Override + public Result disassemble(AnnotationPathNode node) { + return annotationPrinter(node).map(this::print); + } + + @Override + public JavaClassRepresentation getRepresentation(JvmClassInfo info) { + return new JavaClassRepresentation(info.getBytecode()); + } + + @Override + public JvmClassInfo getClassInfo(JavaClassRepresentation representation) { + return new JvmClassInfoBuilder().withBytecode(representation.data()).build(); + } + + @Override + protected Result classPrinter(ClassPathNode node) { + try { + return Result.ok(new JvmClassPrinter(new ByteArrayInputStream(node.getValue().asJvmClass().getBytecode()))); + } catch (IOException e) { + return Result.exception(e); + } + } + + @Override + public AssemblerPipelineConfig getConfig() { + return null; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/PipelineContainer.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/PipelineContainer.java new file mode 100644 index 000000000..7335a6563 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/PipelineContainer.java @@ -0,0 +1,14 @@ +package software.coley.recaf.services.assembler; + +import me.darknet.assembler.compiler.ClassRepresentation; +import software.coley.recaf.info.ClassInfo; + +public class PipelineContainer { + + private final AssemblerPipeline pipeline; + + + public PipelineContainer(AssemblerPipeline pipeline) { + this.pipeline = pipeline; + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/builtin/BasicClassContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/builtin/BasicClassContextMenuProviderFactory.java index 4977f4584..cca8c638c 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/builtin/BasicClassContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/builtin/BasicClassContextMenuProviderFactory.java @@ -13,11 +13,13 @@ import software.coley.recaf.info.AndroidClassInfo; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.path.PathNodes; import software.coley.recaf.services.cell.*; import software.coley.recaf.services.navigation.Actions; import software.coley.recaf.ui.control.ActionMenuItem; import software.coley.recaf.util.ClipboardUtil; import software.coley.recaf.util.Menus; +import software.coley.recaf.util.Unchecked; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.AndroidClassBundle; import software.coley.recaf.workspace.model.bundle.ClassBundle; @@ -146,8 +148,9 @@ private void populateJvmMenu(@Nonnull ContextMenu menu, ActionMenuItem removeAnnotations = action("menu.edit.remove.annotation", CarbonIcons.CLOSE, () -> actions.deleteClassAnnotations(workspace, resource, bundle, info)); // TODO: Implement these operations after assembler is added. // - For add operations, can use the assembler, using a template for each item - ActionMenuItem editClass = action("menu.edit.assemble.class", CarbonIcons.EDIT, () -> { - }); + ActionMenuItem editClass = action("menu.tab.edit", CarbonIcons.EDIT, Unchecked.runnable(() -> + actions.openAssembler(PathNodes.classPath(workspace, resource, bundle, info)) + )); ActionMenuItem addField = action("menu.edit.add.field", CarbonIcons.ADD_ALT, () -> { }); ActionMenuItem addMethod = action("menu.edit.add.method", CarbonIcons.ADD_ALT, () -> { @@ -173,7 +176,6 @@ private void populateJvmMenu(@Nonnull ContextMenu menu, removeAnnotations.setDisable(info.getAnnotations().isEmpty()); // Not implemented yet, so disable - editClass.setDisable(true); addField.setDisable(true); addMethod.setDisable(true); addAnnotation.setDisable(true); diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/builtin/BasicFieldContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/builtin/BasicFieldContextMenuProviderFactory.java index 7773a4e82..30d9b2b0f 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/builtin/BasicFieldContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/builtin/BasicFieldContextMenuProviderFactory.java @@ -20,6 +20,7 @@ import software.coley.recaf.ui.control.ActionMenuItem; import software.coley.recaf.util.ClipboardUtil; import software.coley.recaf.util.Lang; +import software.coley.recaf.util.Unchecked; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.ClassBundle; import software.coley.recaf.workspace.model.resource.WorkspaceResource; @@ -72,6 +73,10 @@ public ContextMenuProvider getFieldContextMenuProvider(@Nonnull ContextSource so } else { items.add(action("menu.tab.copypath", CarbonIcons.COPY_LINK, () -> ClipboardUtil.copyString(declaringClass, field))); + items.add(action("menu.tab.edit", CarbonIcons.EDIT, Unchecked.runnable(() -> + actions.openAssembler(PathNodes.memberPath(workspace, resource, bundle, declaringClass, field)) + ))); + // TODO: implement operations // - Edit // - (field / method assembler) diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/builtin/BasicMethodContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/builtin/BasicMethodContextMenuProviderFactory.java index bb629506f..a6a77b047 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/builtin/BasicMethodContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/builtin/BasicMethodContextMenuProviderFactory.java @@ -18,6 +18,7 @@ import software.coley.recaf.services.navigation.Actions; import software.coley.recaf.ui.control.ActionMenuItem; import software.coley.recaf.util.ClipboardUtil; +import software.coley.recaf.util.Unchecked; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.ClassBundle; import software.coley.recaf.workspace.model.resource.WorkspaceResource; @@ -69,6 +70,10 @@ public ContextMenuProvider getMethodContextMenuProvider(@Nonnull ContextSource s } else { items.add(action("menu.tab.copypath", CarbonIcons.COPY_LINK, () -> ClipboardUtil.copyString(declaringClass, method))); + items.add(action("menu.tab.edit", CarbonIcons.EDIT, Unchecked.runnable(() -> + actions.openAssembler(PathNodes.memberPath(workspace, resource, bundle, declaringClass, method)) + ))); + // TODO: implement additional operations // - Edit // - (field / method assembler) diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/info/association/FileTypeAssociationServiceConfig.java b/recaf-ui/src/main/java/software/coley/recaf/services/info/association/FileTypeAssociationServiceConfig.java index 65dc4a33c..d0c8a3aaa 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/info/association/FileTypeAssociationServiceConfig.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/info/association/FileTypeAssociationServiceConfig.java @@ -39,6 +39,7 @@ public FileTypeAssociationServiceConfig() { extensionsToLangKeys = new ExtensionMapping(List.of( new Pair<>("java", "java"), + new Pair<>("jasm", "jasm"), new Pair<>("xml", "xml"), new Pair<>("html", "xml"), new Pair<>("enigma", "enigma") diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java index 9bd4f7995..1c7413a43 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java @@ -12,6 +12,7 @@ import javafx.scene.control.MenuItem; import javafx.scene.control.Tab; import org.kordamp.ikonli.carbonicons.CarbonIcons; +import org.kordamp.ikonli.javafx.FontIcon; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.slf4j.Logger; @@ -27,6 +28,7 @@ import software.coley.recaf.services.mapping.IntermediateMappings; import software.coley.recaf.services.mapping.MappingApplier; import software.coley.recaf.services.mapping.MappingResults; +import software.coley.recaf.ui.control.FontIconView; import software.coley.recaf.ui.control.popup.ItemListSelectionPopup; import software.coley.recaf.ui.control.popup.ItemTreeSelectionPopup; import software.coley.recaf.ui.control.popup.NamePopup; @@ -34,6 +36,7 @@ import software.coley.recaf.ui.docking.DockingRegion; import software.coley.recaf.ui.docking.DockingTab; import software.coley.recaf.ui.pane.editing.android.AndroidClassPane; +import software.coley.recaf.ui.pane.editing.assembler.AssemblerPane; import software.coley.recaf.ui.pane.editing.binary.BinaryXmlFilePane; import software.coley.recaf.ui.pane.editing.jvm.JvmClassEditorType; import software.coley.recaf.ui.pane.editing.jvm.JvmClassPane; @@ -80,6 +83,7 @@ public class Actions implements Service { private final Instance imagePaneProvider; private final Instance audioPaneProvider; private final Instance videoPaneProvider; + private final Instance assemblerPaneProvider; private final ActionsConfig config; @Inject @@ -95,7 +99,8 @@ public Actions(@Nonnull ActionsConfig config, @Nonnull Instance textPaneProvider, @Nonnull Instance imagePaneProvider, @Nonnull Instance audioPaneProvider, - @Nonnull Instance videoPaneProvider) { + @Nonnull Instance videoPaneProvider, + @Nonnull Instance assemblerPaneProvider) { this.config = config; this.navigationManager = navigationManager; this.dockingManager = dockingManager; @@ -109,6 +114,7 @@ public Actions(@Nonnull ActionsConfig config, this.imagePaneProvider = imagePaneProvider; this.audioPaneProvider = audioPaneProvider; this.videoPaneProvider = videoPaneProvider; + this.assemblerPaneProvider = assemblerPaneProvider; } /** @@ -1282,6 +1288,59 @@ public void copyPackage(@Nonnull Workspace workspace, .show(); } + /** + * Brings a {@link ClassNavigable} component representing the given class into focus. + * If no such component exists, one is created. + *
+ * Automatically calls the type-specific goto-declaration handling. + * + * @param path + * Path containing a class to open. + * + * @return Navigable content representing class content of the path. + * + * @throws IncompletePathException + * When the path is missing parent elements. + */ + @Nonnull + public Navigable openAssembler(@Nonnull PathNode path) throws IncompletePathException { + Workspace workspace = path.getValueOfType(Workspace.class); + WorkspaceResource resource = path.getValueOfType(WorkspaceResource.class); + ClassBundle bundle = path.getValueOfType(ClassBundle.class); + ClassInfo info = path.getValueOfType(ClassInfo.class); + if(info == null) { + logger.error("Cannot resolve required path nodes, missing class in path"); + throw new IncompletePathException(ClassInfo.class); + } + if (workspace == null) { + logger.error("Cannot resolve required path nodes for class '{}', missing workspace in path", info.getName()); + throw new IncompletePathException(Workspace.class); + } + if (resource == null) { + logger.error("Cannot resolve required path nodes for class '{}', missing resource in path", info.getName()); + throw new IncompletePathException(WorkspaceResource.class); + } + if (bundle == null) { + logger.error("Cannot resolve required path nodes for class '{}', missing bundle in path", info.getName()); + throw new IncompletePathException(ClassBundle.class); + } + + return getOrCreatePathContent(path, () -> { + // Create text/graphic for the tab to create. + String title = "Hurp"; // TODO: Get title for path node + Node graphic = new FontIconView(CarbonIcons.CODE); + if (title == null) throw new IllegalStateException("Missing title"); + if (graphic == null) throw new IllegalStateException("Missing graphic"); + + // Create content for the tab. + AssemblerPane content = assemblerPaneProvider.get(); + content.onUpdatePath(path); + + // Build the tab. + return createTab(dockingManager.getPrimaryRegion(), title, graphic, content); + }); + } + /** * Prompts the user (if configured, otherwise prompt is skipped) to delete the class. * diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/LanguageStylesheets.java b/recaf-ui/src/main/java/software/coley/recaf/ui/LanguageStylesheets.java index 07255762a..a6e7a273b 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/LanguageStylesheets.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/LanguageStylesheets.java @@ -20,6 +20,7 @@ public class LanguageStylesheets { private static final Map NAME_TO_PATH = new HashMap<>(); private static final String SHEET_JAVA; + private static final String SHEET_JASM; private static final String SHEET_XML; private static final String SHEET_ENIGMA; @@ -29,6 +30,7 @@ private LanguageStylesheets() { static { SHEET_JAVA = addLanguage("/syntax/java.css"); + SHEET_JASM = addLanguage("/syntax/jasm.css"); SHEET_XML = addLanguage("/syntax/xml.css"); SHEET_ENIGMA = addLanguage("/syntax/enigma.css"); } @@ -95,6 +97,16 @@ public static String getJavaStylesheet() { return SHEET_JAVA; } + /** + * @return Stylesheet for JASM. + * + * @see RegexLanguages#getJasmLanguage() + */ + @Nonnull + public static String getJasmStylesheet() { + return SHEET_JASM; + } + /** * @return Stylesheet for XML. * diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/RegexLanguages.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/RegexLanguages.java index d0d821baa..3e609ace0 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/RegexLanguages.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/RegexLanguages.java @@ -28,6 +28,7 @@ public class RegexLanguages { private static final JsonMapper MAPPER = new JsonMapper(); private static final Map NAME_TO_LANG = new HashMap<>(); private static final RegexRule LANG_JAVA; + private static final RegexRule LANG_JASM; private static final RegexRule LANG_XML; private static final RegexRule LANG_ENGIMA_MAP; @@ -38,6 +39,7 @@ private RegexLanguages() { static { try { LANG_JAVA = addLanguage("/syntax/java.json"); + LANG_JASM = addLanguage("/syntax/jasm.json"); LANG_XML = addLanguage("/syntax/xml.json"); LANG_ENGIMA_MAP = addLanguage("/syntax/enigma.json"); } catch (Exception ex) { @@ -124,6 +126,16 @@ public static RegexRule getJavaLanguage() { return LANG_JAVA; } + /** + * @return Root rule for JASM regex matching. + * + * @see LanguageStylesheets#getJasmStylesheet() + */ + @Nonnull + public static RegexRule getJasmLanguage() { + return LANG_JASM; + } + /** * @return Root rule for XML regex matching. * diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/RegexSyntaxHighlighter.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/RegexSyntaxHighlighter.java index 6cf4ad4ea..74391225d 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/RegexSyntaxHighlighter.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/RegexSyntaxHighlighter.java @@ -185,7 +185,6 @@ public Region(String text, Region parent, RegexRule rule, int start, int end) { this.start = start; this.end = end; } - /** * Splits this node into subregions based on the given rules. * @@ -199,6 +198,9 @@ public void split(List rules) { // Match within give region String localText = text.substring(start, end); + if (localText.isEmpty()) + return; + Matcher matcher = getCombinedPattern(rules).matcher(localText); while (matcher.find()) { // Create region from found match diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java new file mode 100644 index 000000000..3a04d7b5a --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java @@ -0,0 +1,147 @@ +package software.coley.recaf.ui.pane.editing.assembler; + +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Inject; +import me.darknet.assembler.ast.ASTElement; +import me.darknet.assembler.compiler.ClassRepresentation; +import me.darknet.assembler.error.Error; +import me.darknet.assembler.error.Result; +import me.darknet.assembler.parser.Token; +import me.darknet.assembler.util.Location; +import software.coley.recaf.info.ClassInfo; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.PathNode; +import software.coley.recaf.services.assembler.AssemblerPipeline; +import software.coley.recaf.services.assembler.AssemblerPipelineManager; +import software.coley.recaf.services.navigation.NavigationManager; +import software.coley.recaf.ui.LanguageStylesheets; +import software.coley.recaf.ui.control.richtext.Editor; +import software.coley.recaf.ui.control.richtext.bracket.BracketMatchGraphicFactory; +import software.coley.recaf.ui.control.richtext.problem.*; +import software.coley.recaf.ui.control.richtext.syntax.RegexLanguages; +import software.coley.recaf.ui.control.richtext.syntax.RegexSyntaxHighlighter; +import software.coley.recaf.ui.pane.editing.AbstractContentPane; +import software.coley.recaf.util.FxThreadUtil; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +@Dependent +public class AssemblerPane extends AbstractContentPane> { + + private final AssemblerPipelineManager pipelineManager; + private final ProblemTracking problemTracking = new ProblemTracking(); + private final Editor editor = new Editor(); + private AssemblerPipeline pipeline; + private ClassInfo classInfo; + private List lastTokens; + private List lastRoughAst; + private List lastPartialAst; + private List lastAst; + + @Inject + public AssemblerPane(@Nonnull AssemblerPipelineManager pipelineManager) { + this.pipelineManager = pipelineManager; + + this.editor.getCodeArea().getStylesheets().add(LanguageStylesheets.getJasmStylesheet()); + + this.editor.setSyntaxHighlighter(new RegexSyntaxHighlighter(RegexLanguages.getJasmLanguage())); + this.editor.setProblemTracking(problemTracking); + + this.editor.getRootLineGraphicFactory().addLineGraphicFactories( + new BracketMatchGraphicFactory(), + new ProblemGraphicFactory() + ); + + this.editor.getTextChangeEventStream().subscribe(event -> { + parseAST(ast -> {}); + }); + } + + @Nonnull + @Override + public PathNode getPath() { + return path; + } + + @Override + public void onUpdatePath(@Nonnull PathNode path) { + this.path = path; + this.pipeline = pipelineManager.getPipeline(path); + this.classInfo = path.getValueOfType(ClassInfo.class); + refreshDisplay(); + } + + private void disassemble() { + problemTracking.removeByPhase(ProblemPhase.LINT); + + CompletableFuture.supplyAsync(() -> pipeline.disassemble(path)) + .whenCompleteAsync((result, unused) -> + acceptResult(result, editor::setText, ProblemPhase.LINT), FxThreadUtil.executor()); + } + + private void parseAST(Consumer> acceptor) { + if(editor.getText().isBlank()) return; + CompletableFuture.runAsync(() -> { + problemTracking.removeByPhase(ProblemPhase.LINT); + this.lastTokens = pipeline.tokenize(editor.getText(), ""); + + acceptResult(pipeline.roughParse(this.lastTokens), roughAst -> { + this.lastRoughAst = roughAst; + + acceptResult(pipeline.concreteParse(roughAst), ast -> { + this.lastAst = ast; + + acceptor.accept(ast); + }, pAst -> this.lastPartialAst = pAst, ProblemPhase.LINT); + }, pAst -> this.lastPartialAst = pAst, ProblemPhase.LINT); + + this.editor.redrawParagraphGraphics(); + }, FxThreadUtil.executor()); + } + + private void assemble() { + + } + + void processErrors(Collection errors, ProblemPhase phase) { + for (Error error : errors) { + Location location = error.getLocation(); + int line = location == null ? 1 : location.getLine(); + int column = location == null ? 1 : location.getColumn(); + Problem problem = new Problem(line, column, ProblemLevel.ERROR, phase, + error.getMessage()); + problemTracking.add(problem); + + Throwable trace = new Throwable(); + trace.setStackTrace(error.getInCodeSource()); + trace.printStackTrace(); + + System.err.println(error); + } + if(!errors.isEmpty()) + this.editor.redrawParagraphGraphics(); + } + + void acceptResult(Result result, Consumer acceptor, ProblemPhase phase) { + result.ifOk(acceptor).ifErr(errors -> processErrors(errors, phase)); + } + + void acceptResult(Result result, Consumer acceptor, Consumer pAcceptor, ProblemPhase phase) { + result.ifOk(acceptor).ifErr((pOk, errors) -> { + pAcceptor.accept(pOk); + processErrors(errors, phase); + }); + } + + @Override + protected void generateDisplay() { + disassemble(); + + setDisplay(editor); + } +} diff --git a/recaf-ui/src/main/resources/syntax/jasm.css b/recaf-ui/src/main/resources/syntax/jasm.css new file mode 100644 index 000000000..2bc681000 --- /dev/null +++ b/recaf-ui/src/main/resources/syntax/jasm.css @@ -0,0 +1,15 @@ +.code-area .keyword { + -fx-fill: rgb(0, 175, 255); +} + +.code-area .string { + -fx-fill: rgb(175, 215, 223); +} + +.code-area .constant { + -fx-fill: rgb(125, 215, 215); +} + +.code-area .comment-line { + -fx-fill: rgb(175, 175, 215); +} \ No newline at end of file diff --git a/recaf-ui/src/main/resources/syntax/jasm.json b/recaf-ui/src/main/resources/syntax/jasm.json new file mode 100644 index 000000000..23c450102 --- /dev/null +++ b/recaf-ui/src/main/resources/syntax/jasm.json @@ -0,0 +1,39 @@ +{ + "name": "Java bytecode Language", + "regex": "", + "classes": [], + "sub-rules": [ + { + "name": "SingleLineComment", + "regex": "//[^\\n]*", + "classes": [ + "comment-line" + ], + "sub-rules": [] + }, + { + "name": "Constants", + "regex": "\\b0(?:[xX][0-9a-fA-F]+|b[01]+|[0-7]+)\\b|\\b(?:[\\d_]+\\.\\d+|[\\d_]+)(?:[eE]-?[\\d_]+)?[fFdDlL]?\\b|\\b(?:true|false|null|NaN|\\+Infinity|\\-Infinity)\\b|'[\\\\]?.'", + "classes": [ + "constant" + ], + "sub-rules": [] + }, + { + "name": "Strings", + "regex": "(?:\\\"(?:[^\\n\"\\\\]|\\\\.)*?\\\")|(?:\\'(?:[^'\\n\\\\]|\\\\.)*?\\')", + "classes": [ + "string" + ], + "sub-rules": [] + }, + { + "name": "Keywords", + "regex": "\\b(?:method|field|class|annotation|public|private|native|abstract|interface|synthetic|strict|enum|super|module|synchronizer|bridge|varargs|volatile|transient|static|final|protected|aaload|aastore|aconst_null|aload|aload_0|aload_1|aload_2|aload_3|anewarray|areturn|arraylength|astore|astore_0|astore_1|astore_2|astore_3|athrow|baload|bastore|bipush|caload|castore|checkcast|d2f|d2i|d2l|dadd|daload|dastore|dcmpg|dcmpl|dconst_0|dconst_1|ddiv|dload|dload_0|dload_1|dload_2|dload_3|dmul|dneg|drem|dreturn|dstore|dstore_0|dstore_1|dstore_2|dstore_3|dsub|dup|dup_x1|dup_x2|dup2|dup2_x1|dup2_x2|f2d|f2i|f2l|fadd|faload|fastore|fcmpg|fcmpl|fconst_0|fconst_1|fconst_2|fdiv|fload|fload_0|fload_1|fload_2|fload_3|fmul|fneg|frem|freturn|fstore|fstore_0|fstore_1|fstore_2|fstore_3|fsub|getfield|getstatic|goto|goto_w|i2b|i2c|i2d|i2f|i2l|i2s|iadd|iaload|iand|iastore|iconst_0|iconst_1|iconst_2|iconst_3|iconst_4|iconst_5|iconst_m1|idiv|if_acmpeq|if_acmpne|if_icmple|if_icmpeq|if_icmpne|if_icmplt|if_icmpge|if_icmpgt|if_icmple|ifeq|ifne|iflt|ifge|ifgt|ifle|ifnonnull|ifnull|iinc|iload|iload_0|iload_1|iload_2|iload_3|imul|ineg|instanceof|invokedynamic|invokeinterface|invokespecial|invokestatic|invokevirtual|ior|irem|ireturn|ishl|ishr|istore|istore_0|istore_1|istore_2|istore_3|isub|iushr|ixor|jsr|jsr_w|l2d|l2f|l2i|ladd|laload|land|lastore|lcmp|lconst_0|lconst_1|ldc|ldc_w|ldc2_w|ldiv|lload|lload_0|lload_1|lload_2|lload_3|lmul|lneg|lookupswitch|lor|lrem|lreturn|lshl|lshr|lstore|lstore_0|lstore_1|lstore_2|lstore_3|lsub|lushr|lxor|monitorenter|monitorexit|multianewarray|new|newarray|nop|pop|pop2|putfield|putstatic|ret|return|saload|sastore|sipush|swap|tableswitch|wide)\\b", + "classes": [ + "keyword" + ], + "sub-rules": [] + } + ] +} \ No newline at end of file From ba0c3dc557b5ad9337f67fe7b81654c390c02047 Mon Sep 17 00:00:00 2001 From: Justus Garbe Date: Thu, 21 Sep 2023 17:04:23 +0200 Subject: [PATCH 3/5] Basic jasm compiling --- .../assembler/AbstractAssemblerPipeline.java | 68 ++++++++++++++ .../services/assembler/AssemblerPipeline.java | 4 +- .../AssemblerPipelineGeneralConfig.java | 7 ++ .../assembler/AssemblerPipelineManager.java | 2 +- .../assembler/JvmAssemblerPipeline.java | 49 +++++++++- .../pane/editing/assembler/AssemblerPane.java | 94 +++++++++++++++---- 6 files changed, 197 insertions(+), 27 deletions(-) diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AbstractAssemblerPipeline.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AbstractAssemblerPipeline.java index 49bba7ac8..d955155ca 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AbstractAssemblerPipeline.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AbstractAssemblerPipeline.java @@ -1,7 +1,12 @@ package software.coley.recaf.services.assembler; import com.google.common.reflect.ClassPath; +import me.darknet.assembler.ast.ASTElement; +import me.darknet.assembler.ast.ElementType; import me.darknet.assembler.compiler.ClassRepresentation; +import me.darknet.assembler.compiler.Compiler; +import me.darknet.assembler.compiler.CompilerOptions; +import me.darknet.assembler.compiler.InheritanceChecker; import me.darknet.assembler.error.Error; import me.darknet.assembler.error.Result; import me.darknet.assembler.printer.*; @@ -15,6 +20,8 @@ import software.coley.recaf.path.ClassPathNode; import software.coley.recaf.path.PathNode; +import java.util.List; + public abstract class AbstractAssemblerPipeline implements AssemblerPipeline { @@ -29,6 +36,67 @@ public AbstractAssemblerPipeline(AssemblerPipelineGeneralConfig config, Assemble } protected abstract Result classPrinter(ClassPathNode location); + protected abstract CompilerOptions> getCompilerOptions(); + protected abstract Compiler getCompiler(); + protected abstract InheritanceChecker getInheritanceChecker(); + protected abstract int getClassVersion(C info); + + protected Result compile(List elements, PathNode node) { + if(elements.isEmpty()) { + return Result.err(Error.of("No elements to compile", null)); + } + if(elements.size() != 1) { + return Result.err(Error.of("Multiple elements to compile", elements.get(1).location())); + } + ASTElement element = elements.get(0); + + if(element == null) { + return Result.err(Error.of("No element to compile", null)); + } + + ClassInfo classInfo = node.getValueOfType(ClassInfo.class); + if(classInfo == null) { + return Result.err(Error.of("Dangling member", null)); + } + + C info = (C) classInfo; + + CompilerOptions> options = getCompilerOptions(); + options.version(getClassVersion(info)) + .inheritanceChecker(getInheritanceChecker()); + + if(element.type() != ElementType.CLASS) { + options.overlay(getRepresentation(info)); + + if(element.type() == ElementType.ANNOTATION) { + // build annotation path + String path = "this"; + PathNode parent = node.getParent(); + if(parent instanceof ClassMemberPathNode memberPathNode) { + path += memberPathNode.isMethod() ? ".method." : ".field."; + path += memberPathNode.getValue().getName() + "."; + path += memberPathNode.getValue().getDescriptor(); + } + + Annotated annotated = node.getValueOfType(Annotated.class); + + if(annotated == null) { + return Result.err(Error.of("Dangling annotation", null)); + } + + AnnotationInfo annotation = (AnnotationInfo) node.getValue(); + + path += annotated.getAnnotations().indexOf(annotation); + + options.annotationPath(path); + + } + } + + Compiler compiler = getCompiler(); + + return compiler.compile(elements, options).flatMap(res -> Result.ok(getClassInfo((R) res))); + } protected Result memberPrinter(ClassMemberPathNode node) { ClassPathNode owner = node.getParent(); diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipeline.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipeline.java index 1a465874c..49e1756d7 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipeline.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipeline.java @@ -23,7 +23,7 @@ public interface AssemblerPipeline { - default List tokenize(String input, String source) { + default Result> tokenize(String input, String source) { return new Tokenizer().tokenize(source, input); } @@ -35,7 +35,7 @@ default ParsingResult> roughParse(List tokens) { Result> pipe(List tokens, C info); - Result assemble(List elements, C info); + Result assemble(List elements, PathNode node); Result disassemble(ClassPathNode node); diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineGeneralConfig.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineGeneralConfig.java index 0f8e0e523..7bce43c2c 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineGeneralConfig.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineGeneralConfig.java @@ -2,6 +2,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import software.coley.observables.ObservableInteger; import software.coley.observables.ObservableString; import software.coley.recaf.config.BasicConfigContainer; import software.coley.recaf.config.BasicConfigValue; @@ -12,6 +13,7 @@ public class AssemblerPipelineGeneralConfig extends BasicConfigContainer implements ServiceConfig { private final ObservableString disassemblyIndent = new ObservableString(" "); + private final ObservableInteger disassemblyAstParseDelay = new ObservableInteger(100); @Inject public AssemblerPipelineGeneralConfig() { @@ -19,9 +21,14 @@ public AssemblerPipelineGeneralConfig() { + "general" + CONFIG_SUFFIX); addValue(new BasicConfigValue<>("disassembly_indent", String.class, disassemblyIndent)); + addValue(new BasicConfigValue<>("disassembly_ast_parse_delay", Integer.class, disassemblyAstParseDelay)); } public ObservableString getDisassemblyIndent() { return disassemblyIndent; } + + public ObservableInteger getDisassemblyAstParseDelay() { + return disassemblyAstParseDelay; + } } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineManager.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineManager.java index df80e45f2..13914721b 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineManager.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/AssemblerPipelineManager.java @@ -33,7 +33,7 @@ public String getServiceId() { @Nonnull @Override - public ServiceConfig getServiceConfig() { + public AssemblerPipelineGeneralConfig getServiceConfig() { return config; } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/JvmAssemblerPipeline.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/JvmAssemblerPipeline.java index 7149cd981..57a45c36b 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/assembler/JvmAssemblerPipeline.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/JvmAssemblerPipeline.java @@ -6,6 +6,10 @@ import me.darknet.assembler.ast.ASTElement; import me.darknet.assembler.compile.JavaClassRepresentation; import me.darknet.assembler.compile.JvmCompiler; +import me.darknet.assembler.compile.JvmCompilerOptions; +import me.darknet.assembler.compiler.Compiler; +import me.darknet.assembler.compiler.CompilerOptions; +import me.darknet.assembler.compiler.InheritanceChecker; import me.darknet.assembler.error.Error; import me.darknet.assembler.error.Result; import me.darknet.assembler.parser.BytecodeFormat; @@ -15,6 +19,7 @@ import me.darknet.assembler.printer.JvmClassPrinter; import me.darknet.assembler.printer.MemberPrinter; import me.darknet.assembler.printer.Printer; +import org.objectweb.asm.ClassReader; import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.info.annotation.Annotated; import software.coley.recaf.info.annotation.AnnotationInfo; @@ -23,6 +28,9 @@ import software.coley.recaf.path.AnnotationPathNode; import software.coley.recaf.path.ClassMemberPathNode; import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.PathNode; +import software.coley.recaf.services.inheritance.InheritanceGraph; +import software.coley.recaf.services.inheritance.InheritanceVertex; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -32,11 +40,14 @@ public class JvmAssemblerPipeline extends AbstractAssemblerPipeline { private final ASTProcessor processor = new ASTProcessor(BytecodeFormat.JVM); + private final InheritanceGraph inheritanceGraph; private final String name = "JVM"; @Inject - public JvmAssemblerPipeline(@Nonnull AssemblerPipelineGeneralConfig config) { + public JvmAssemblerPipeline(@Nonnull AssemblerPipelineGeneralConfig config, + @Nonnull InheritanceGraph inheritanceGraph) { super(config, null); + this.inheritanceGraph = inheritanceGraph; } @Override @@ -55,9 +66,8 @@ public Result> pipe(List tokens, JvmClassInfo info) { } @Override - public Result assemble(List elements, JvmClassInfo info) { - JvmCompiler compiler = new JvmCompiler(); - return null; + public Result assemble(List elements, PathNode node) { + return compile(elements, node); } @Override @@ -80,9 +90,38 @@ public JavaClassRepresentation getRepresentation(JvmClassInfo info) { return new JavaClassRepresentation(info.getBytecode()); } + @Override + protected CompilerOptions> getCompilerOptions() { + return new JvmCompilerOptions(); + } + + @Override + protected Compiler getCompiler() { + return new JvmCompiler(); + } + + @Override + protected InheritanceChecker getInheritanceChecker() { + return (child, parent) -> { + InheritanceVertex childVertex = inheritanceGraph.getVertex(child); + InheritanceVertex parentVertex = inheritanceGraph.getVertex(parent); + + if(childVertex == null || parentVertex == null) { + return false; + } + + return childVertex.isChildOf(parentVertex); + }; + } + + @Override + protected int getClassVersion(JvmClassInfo info) { + return info.getVersion(); + } + @Override public JvmClassInfo getClassInfo(JavaClassRepresentation representation) { - return new JvmClassInfoBuilder().withBytecode(representation.data()).build(); + return new JvmClassInfoBuilder(new ClassReader(representation.data())).build(); } @Override diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java index 3a04d7b5a..e54dc8cb1 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java @@ -9,43 +9,53 @@ import me.darknet.assembler.error.Result; import me.darknet.assembler.parser.Token; import me.darknet.assembler.util.Location; +import org.slf4j.Logger; +import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.ClassInfo; -import software.coley.recaf.path.ClassPathNode; import software.coley.recaf.path.PathNode; import software.coley.recaf.services.assembler.AssemblerPipeline; import software.coley.recaf.services.assembler.AssemblerPipelineManager; -import software.coley.recaf.services.navigation.NavigationManager; +import software.coley.recaf.services.navigation.UpdatableNavigable; import software.coley.recaf.ui.LanguageStylesheets; +import software.coley.recaf.ui.config.KeybindingConfig; import software.coley.recaf.ui.control.richtext.Editor; import software.coley.recaf.ui.control.richtext.bracket.BracketMatchGraphicFactory; import software.coley.recaf.ui.control.richtext.problem.*; import software.coley.recaf.ui.control.richtext.syntax.RegexLanguages; import software.coley.recaf.ui.control.richtext.syntax.RegexSyntaxHighlighter; import software.coley.recaf.ui.pane.editing.AbstractContentPane; +import software.coley.recaf.util.Animations; import software.coley.recaf.util.FxThreadUtil; +import software.coley.recaf.workspace.model.bundle.Bundle; import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.function.BiConsumer; import java.util.function.Consumer; @Dependent -public class AssemblerPane extends AbstractContentPane> { +public class AssemblerPane extends AbstractContentPane> implements UpdatableNavigable { + + private static final Logger logger = Logging.get(AssemblerPane.class); private final AssemblerPipelineManager pipelineManager; + private final KeybindingConfig keys; private final ProblemTracking problemTracking = new ProblemTracking(); private final Editor editor = new Editor(); + private static int timeToWait = 100; private AssemblerPipeline pipeline; private ClassInfo classInfo; private List lastTokens; private List lastRoughAst; private List lastPartialAst; private List lastAst; + private long lastAstParse = 0; @Inject - public AssemblerPane(@Nonnull AssemblerPipelineManager pipelineManager) { + public AssemblerPane(@Nonnull AssemblerPipelineManager pipelineManager, + @Nonnull KeybindingConfig keys) { this.pipelineManager = pipelineManager; + this.keys = keys; this.editor.getCodeArea().getStylesheets().add(LanguageStylesheets.getJasmStylesheet()); @@ -57,8 +67,14 @@ public AssemblerPane(@Nonnull AssemblerPipelineManager pipelineManager) { new ProblemGraphicFactory() ); - this.editor.getTextChangeEventStream().subscribe(event -> { - parseAST(ast -> {}); + this.editor.getTextChangeEventStream().subscribe(event -> parseAST(ast -> {})); + + this.pipelineManager.getServiceConfig().getDisassemblyAstParseDelay().addChangeListener( + (observable, oldVal, newVal) -> timeToWait = newVal); + + setOnKeyPressed(event -> { + if(this.keys.getSave().match(event)) + assemble(); }); } @@ -86,26 +102,65 @@ private void disassemble() { private void parseAST(Consumer> acceptor) { if(editor.getText().isBlank()) return; + if(lastAstParse > System.currentTimeMillis() - timeToWait) return; + lastAstParse = System.currentTimeMillis(); CompletableFuture.runAsync(() -> { - problemTracking.removeByPhase(ProblemPhase.LINT); - this.lastTokens = pipeline.tokenize(editor.getText(), ""); + try { + problemTracking.removeByPhase(ProblemPhase.LINT); + + Result> tokenResult = pipeline.tokenize(editor.getText(), ""); - acceptResult(pipeline.roughParse(this.lastTokens), roughAst -> { - this.lastRoughAst = roughAst; + if(tokenResult.hasErr()) + processErrors(tokenResult.errors(), ProblemPhase.LINT); - acceptResult(pipeline.concreteParse(roughAst), ast -> { - this.lastAst = ast; + this.lastTokens = tokenResult.get(); - acceptor.accept(ast); + acceptResult(pipeline.roughParse(this.lastTokens), roughAst -> { + this.lastRoughAst = roughAst; + + acceptResult(pipeline.concreteParse(roughAst), ast -> { + this.lastAst = ast; + + acceptor.accept(ast); + }, pAst -> this.lastPartialAst = pAst, ProblemPhase.LINT); }, pAst -> this.lastPartialAst = pAst, ProblemPhase.LINT); - }, pAst -> this.lastPartialAst = pAst, ProblemPhase.LINT); - this.editor.redrawParagraphGraphics(); + this.editor.redrawParagraphGraphics(); + } catch (Exception ex) { + logger.error("Failed to parse assembler", ex); + } }, FxThreadUtil.executor()); } private void assemble() { + if(!problemTracking.getProblems().isEmpty()) + return; + CompletableFuture.runAsync(() -> { + try { + if(lastAst == null || lastAstParse < System.currentTimeMillis() - timeToWait) { + parseAST(ast -> assemble()); + return; + } + + problemTracking.removeByPhase(ProblemPhase.BUILD); + + pipeline.assemble(lastAst, path).ifOk(info -> { + this.classInfo = info; + + Bundle bundle = path.getValueOfType(Bundle.class); + bundle.put(info); + + Animations.animateSuccess(editor, 1000); + }).ifErr(errors -> { + processErrors(errors, ProblemPhase.BUILD); + + Animations.animateFailure(editor, 1000); + }); + } catch (Exception ex) { + logger.error("Failed to assemble", ex); + } + }, FxThreadUtil.executor()); } void processErrors(Collection errors, ProblemPhase phase) { @@ -117,11 +172,12 @@ void processErrors(Collection errors, ProblemPhase phase) { error.getMessage()); problemTracking.add(problem); - Throwable trace = new Throwable(); + // REMOVE IS TRACING PARSER ERRORS + /*Throwable trace = new Throwable(); trace.setStackTrace(error.getInCodeSource()); - trace.printStackTrace(); + logger.trace("Assembler error", trace); - System.err.println(error); + System.err.println(error);*/ } if(!errors.isEmpty()) this.editor.redrawParagraphGraphics(); From c439852bd2253cd120c6d8164aaf45b1702c53ec Mon Sep 17 00:00:00 2001 From: Justus Garbe Date: Thu, 21 Sep 2023 17:25:06 +0200 Subject: [PATCH 4/5] Change over to remote maven --- dependencies.gradle | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 90a94340c..b6da49fb8 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,6 +1,6 @@ def asmVersion = '9.5' def cafeDudeVersion = '1.10.2' -def jasmVersion = '8663cec34f' +def jasmVersion = '2.0.0' def junitVersion = '5.9.2' project.ext { @@ -39,10 +39,8 @@ project.ext { instrument_server = 'software.coley:instrumentation-server:1.3.6' - //jasm_core = "com.github.Nowilltolife.Jasm:jasm-core:$jasmVersion" - //jasm_composistion_jvm = "com.github.Nowilltolife.Jasm:jasm-composition-jvm:$jasmVersion" - jasm_core = "me.darknet:jasm-core:2.0.0" - jasm_composistion_jvm = "me.darknet:jasm-composition-jvm:2.0.0" + jasm_core = "com.github.Nowilltolife.Jasm:jasm-core:$jasmVersion" + jasm_composistion_jvm = "com.github.Nowilltolife.Jasm:jasm-composition-jvm:$jasmVersion" jackson = 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.15.2' From d36dc5afeba3349c428731e60641c7e984f55c18 Mon Sep 17 00:00:00 2001 From: Justus Garbe Date: Thu, 21 Sep 2023 17:47:25 +0200 Subject: [PATCH 5/5] Add better parsing wait --- .../ui/pane/editing/assembler/AssemblerPane.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java index e54dc8cb1..213460a46 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java @@ -28,6 +28,7 @@ import software.coley.recaf.util.FxThreadUtil; import software.coley.recaf.workspace.model.bundle.Bundle; +import java.time.Duration; import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -49,7 +50,6 @@ public class AssemblerPane extends AbstractContentPane> implements U private List lastRoughAst; private List lastPartialAst; private List lastAst; - private long lastAstParse = 0; @Inject public AssemblerPane(@Nonnull AssemblerPipelineManager pipelineManager, @@ -67,7 +67,9 @@ public AssemblerPane(@Nonnull AssemblerPipelineManager pipelineManager, new ProblemGraphicFactory() ); - this.editor.getTextChangeEventStream().subscribe(event -> parseAST(ast -> {})); + this.editor.getTextChangeEventStream().successionEnds(Duration.ofMillis(timeToWait)).addObserver(e -> { + parseAST(ast -> {}); + }); this.pipelineManager.getServiceConfig().getDisassemblyAstParseDelay().addChangeListener( (observable, oldVal, newVal) -> timeToWait = newVal); @@ -102,8 +104,7 @@ private void disassemble() { private void parseAST(Consumer> acceptor) { if(editor.getText().isBlank()) return; - if(lastAstParse > System.currentTimeMillis() - timeToWait) return; - lastAstParse = System.currentTimeMillis(); + CompletableFuture.runAsync(() -> { try { problemTracking.removeByPhase(ProblemPhase.LINT); @@ -137,10 +138,10 @@ private void assemble() { return; CompletableFuture.runAsync(() -> { try { - if(lastAst == null || lastAstParse < System.currentTimeMillis() - timeToWait) { - parseAST(ast -> assemble()); + parseAST(ast -> {}); + + if(!problemTracking.getProblems().isEmpty() && lastAst == null) return; - } problemTracking.removeByPhase(ProblemPhase.BUILD);