diff --git a/.github/workflows/maven.yaml b/.github/workflows/maven.yaml index bb699ea4..ed99e349 100644 --- a/.github/workflows/maven.yaml +++ b/.github/workflows/maven.yaml @@ -74,6 +74,7 @@ jobs: shell: cmd - name: Test Cli + timeout-minutes: 10 run: | cd jrd - mvn --batch-mode test -Dtest=CliTest -DfailIfNoTests=false + mvn --batch-mode test -Dtest=*CliTest -DfailIfNoTests=false "-Dsurefire.reportFormat=plain" diff --git a/runtime-decompiler/src/main/java/org/jrd/backend/data/Cli.java b/runtime-decompiler/src/main/java/org/jrd/backend/data/Cli.java index 99b9ab29..0a5839c8 100644 --- a/runtime-decompiler/src/main/java/org/jrd/backend/data/Cli.java +++ b/runtime-decompiler/src/main/java/org/jrd/backend/data/Cli.java @@ -1,5 +1,6 @@ package org.jrd.backend.data; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.github.mkoncek.classpathless.api.ClassIdentifier; import io.github.mkoncek.classpathless.api.ClassesProvider; import io.github.mkoncek.classpathless.api.ClasspathlessCompiler; @@ -25,6 +26,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; +import java.io.PrintStream; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -54,21 +56,25 @@ public class Cli { protected static final String HELP = "-help"; protected static final String H = "-h"; + protected static final String R = "-r"; + protected static final String P = "-p"; + protected static final String CP = "-cp"; + private final List filteredArgs; private final VmManager vmManager; private final PluginManager pluginManager; private Saving saving; private boolean isVerbose; - protected static class Saving implements CommonUtils.StatusKeeper { - protected static final String DEFAULT = "default"; - protected static final String EXACT = "exact"; - protected static final String FQN = "fqn"; - protected static final String DIR = "dir"; + static class Saving implements CommonUtils.StatusKeeper { + static final String DEFAULT = "default"; + static final String EXACT = "exact"; + static final String FQN = "fqn"; + static final String DIR = "dir"; private final String as; private final String like; - public Saving(String as, String like) { + Saving(String as, String like) { this.as = as; if (like == null) { this.like = DEFAULT; @@ -116,6 +122,10 @@ public int toInt(String suffix) { throw new RuntimeException("Unknown saving type: " + like + ". Allowed are: " + FQN + "," + DIR + "," + EXACT); } } + + public PrintStream openPrintStream() throws IOException { + return new PrintStream(new FileOutputStream(this.as), true, "UTF-8"); + } } public Cli(String[] orig, Model model) { @@ -258,13 +268,28 @@ private void overwrite() throws Exception { } } + @SuppressFBWarnings(value = "OS_OPEN_STREAM", justification = "The stream is clsoed as conditionally as is created") private void api() throws Exception { - if (filteredArgs.size() != 2) { - throw new IllegalArgumentException("Incorrect argument count! Please use '" + Help.API_FORMAT + "'."); + PrintStream out = System.out; + try { + if (saving != null && saving.as != null) { + out = saving.openPrintStream(); + } + + if (filteredArgs.size() != 2) { + throw new IllegalArgumentException("Incorrect argument count! Please use '" + Help.API_FORMAT + "'."); + } + + VmInfo vmInfo = getVmInfo(filteredArgs.get(1)); + AgentApiGenerator.initItems(vmInfo, vmManager, pluginManager); + out.println(AgentApiGenerator.getInterestingHelp()); + + out.flush(); + } finally { + if (saving != null && saving.as != null) { + out.close(); + } } - VmInfo vmInfo = getVmInfo(filteredArgs.get(1)); - AgentApiGenerator.initItems(vmInfo, vmManager, pluginManager); - System.out.println(AgentApiGenerator.getInterestingHelp()); } private void init() throws Exception { @@ -299,13 +324,13 @@ private final class CompileArguments { for (int i = 1; i < filteredArgs.size(); i++) { String arg = filteredArgs.get(i); - if ("-p".equals(arg)) { + if (P.equals(arg)) { wantedCustomCompiler = filteredArgs.get(i + 1); i++; // shift - } else if ("-cp".equals(arg)) { + } else if (CP.equals(arg)) { puc = filteredArgs.get(i + 1); i++; // shift - } else if ("-r".equals(arg)) { + } else if (R.equals(arg)) { isRecursive = true; } else { File fileToCompile = new File(arg); @@ -684,26 +709,54 @@ private static boolean matchesAtLeastOne(String clazz, List filter) { return false; } - private void listPlugins() { + @SuppressFBWarnings(value = "OS_OPEN_STREAM", justification = "The stream is clsoed as conditionally as is created") + private void listPlugins() throws IOException { if (filteredArgs.size() != 1) { throw new RuntimeException(LIST_PLUGINS + " does not expect arguments."); } - for (DecompilerWrapper dw : pluginManager.getWrappers()) { - System.out.printf( - "%s %s/%s - %s%n", - dw.getName(), dw.getScope(), invalidityToString(dw.isInvalidWrapper()), dw.getFileLocation() - ); + PrintStream out = System.out; + try { + if (saving != null && saving.as != null) { + out = saving.openPrintStream(); + } + + for (DecompilerWrapper dw : pluginManager.getWrappers()) { + out.printf( + "%s %s/%s - %s%n", + dw.getName(), dw.getScope(), invalidityToString(dw.isInvalidWrapper()), dw.getFileLocation() + ); + } + + out.flush(); + } finally { + if (saving != null && saving.as != null) { + out.close(); + } } } - private void listJvms() { + @SuppressFBWarnings(value = "OS_OPEN_STREAM", justification = "The stream is clsoed as conditionally as is created") + private void listJvms() throws IOException { if (filteredArgs.size() != 1) { throw new RuntimeException(LIST_JVMS + " does not expect arguments."); } - for (VmInfo vmInfo : vmManager.getVmInfoSet()) { - System.out.println(vmInfo.getVmPid() + " " + vmInfo.getVmName()); + PrintStream out = System.out; + try { + if (saving != null && saving.as != null) { + out = saving.openPrintStream(); + } + + for (VmInfo vmInfo : vmManager.getVmInfoSet()) { + out.println(vmInfo.getVmPid() + " " + vmInfo.getVmName()); + } + + out.flush(); + } finally { + if (saving != null && saving.as != null) { + out.close(); + } } } diff --git a/runtime-decompiler/src/main/java/org/jrd/backend/data/Help.java b/runtime-decompiler/src/main/java/org/jrd/backend/data/Help.java index 940de2c7..229edcae 100644 --- a/runtime-decompiler/src/main/java/org/jrd/backend/data/Help.java +++ b/runtime-decompiler/src/main/java/org/jrd/backend/data/Help.java @@ -22,7 +22,7 @@ public final class Help { static final String LIST_JVMS_FORMAT = LIST_JVMS; static final String LIST_PLUGINS_FORMAT = LIST_PLUGINS; static final String LIST_CLASSES_FORMAT = LIST_CLASSES + " [...]"; - static final String COMPILE_FORMAT = COMPILE + " [-p ] [-cp ] [-r] ..."; + static final String COMPILE_FORMAT = COMPILE + " [" + P + " ] [" + CP + " ] [" + R + "] ..."; static final String DECOMPILE_FORMAT = DECOMPILE + " ..."; static final String OVERWRITE_FORMAT = OVERWRITE + " []"; static final String INIT_FORMAT = INIT + " "; @@ -40,9 +40,9 @@ public final class Help { private static final String LIST_CLASSES_TEXT = "List all loaded classes of a process, optionally filtering them.\n" + "Only '" + SAVE_LIKE + " " + Saving.EXACT + "' or '" + SAVE_LIKE + " " + Saving.DEFAULT + "' are allowed as saving modifiers."; - private static final String COMPILE_TEXT = "Compile local files against runtime classpath, specified by -cp.\n" + - "Use -p to utilize some plugins' (like jasm or jcoder) bundled compilers.\n" + - "Use -r for recursive search if is a directory.\n" + + private static final String COMPILE_TEXT = "Compile local files against runtime classpath, specified by " + CP + ".\n" + + "Use " + P + " to utilize some plugins' (like jasm or jcoder) bundled compilers.\n" + + "Use " + R + " for recursive search if is a directory.\n" + "If the argument of '" + SAVE_AS + "' is a valid PID or URL, " + "the compiled code will be attempted to be injected into that process.\n" + "If multiple PATHs were specified, but no '" + SAVE_AS + "', the process fails."; @@ -109,8 +109,8 @@ public final class Help { NOTES.put(NOTES_SAVE, NOTES_SAVE_ITEMS); } - private static final String[] UNSAVABLE_OPTIONS = {HELP, H, LIST_JVMS, LIST_PLUGINS, OVERWRITE, INIT, API}; - private static final String[] SAVABLE_OPTIONS = {LIST_CLASSES, BYTES, BASE64, COMPILE, DECOMPILE}; + private static final String[] UNSAVABLE_OPTIONS = {HELP, H, OVERWRITE, INIT}; + private static final String[] SAVABLE_OPTIONS = {LIST_CLASSES, BYTES, BASE64, COMPILE, DECOMPILE, API, LIST_JVMS, LIST_PLUGINS}; private static final int LONGEST_FORMAT_LENGTH = Stream.of(ALL_OPTIONS.keySet(), SAVING_OPTIONS.keySet()) diff --git a/runtime-decompiler/src/test/java/org/jrd/backend/data/AbstractAgentNeedingTest.java b/runtime-decompiler/src/test/java/org/jrd/backend/data/AbstractAgentNeedingTest.java new file mode 100644 index 00000000..766a2c79 --- /dev/null +++ b/runtime-decompiler/src/test/java/org/jrd/backend/data/AbstractAgentNeedingTest.java @@ -0,0 +1,223 @@ +package org.jrd.backend.data; + +import org.jrd.backend.core.AgentRequestAction; +import org.jrd.frontend.frame.main.DecompilationController; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Timeout; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public abstract class AbstractAgentNeedingTest { + + protected Model model; + protected AbstractSourceTestClass dummy; + protected final JunitStderrOutThief streams = new JunitStderrOutThief(); + + abstract AbstractSourceTestClass dummyProvider() throws AbstractSourceTestClass.SourceTestClassWrapperException; + + @BeforeAll + static void startup() { + String agentPath = Config.getConfig().getAgentExpandedPath(); + + Assumptions.assumeTrue( + !agentPath.isEmpty(), + "Agent path is not set up, aborting CliTest." + ); + Assumptions.assumeTrue( + new File(agentPath).exists(), + "Agent path is set up to nonexistent file, aborting CliTest." + ); + } + + @Timeout(5) + @BeforeEach + void setup() throws InterruptedException { + try { + dummy = dummyProvider(); + } catch (AbstractSourceTestClass.SourceTestClassWrapperException e) { + Assertions.fail(e); + } + Assertions.assertTrue(dummy.isAlive()); + + model = new Model(); // must be below dummy process execution to be aware of it during VmManager instantiation + while (model.getVmManager().findVmFromPidNoException(dummy.getPid()) == null) { + Thread.sleep(100); + model.getVmManager().updateLocalVMs(); + } + + streams.captureStreams(true); + } + + @AfterEach + void cleanup() { + streams.captureStreams(false); + + Assertions.assertTrue(dummy.isAlive()); + // halt agent, otherwise an open socket prevents termination of dummy process + AgentRequestAction request = DecompilationController.createRequest( + model.getVmManager().findVmFromPid(dummy.getPid()), AgentRequestAction.RequestAction.HALT, "" + ); + String response = DecompilationController.submitRequest(model.getVmManager(), request); + Assertions.assertEquals("ok", response); + + Assertions.assertTrue(dummy.isAlive()); + dummy.terminate(); + } + + private static boolean isDifferenceTolerable(double samenessPercentage, int actualChanges, int totalSize) { + assert samenessPercentage >= 0 && samenessPercentage <= 1.0; + + double changesAllowed = (1.0 - samenessPercentage) * totalSize; + return actualChanges <= changesAllowed; + } + + static void assertEqualsWithTolerance(String s1, String s2, double samenessPercentage) { + Assertions.assertTrue(isDifferenceTolerable( + samenessPercentage, + LevenshteinDistance.calculate(s1, s2), + Math.max(s1.length(), s2.length()) + )); + } + + static void assertEqualsWithTolerance(List l1, List l2, double samenessPercentage) { + // symmetric difference + Set intersection = new HashSet<>(l1); + intersection.retainAll(l2); + + Set difference = new HashSet<>(); + difference.addAll(l1); + difference.addAll(l2); + difference.removeAll(intersection); + + Assertions.assertTrue(isDifferenceTolerable(samenessPercentage, difference.size(), Math.max(l1.size(), l2.size()))); + } + + public static final class LevenshteinDistance { + /** + * Calculates the Levenshtein distance between two strings.
+ * Uses a 2D array to represent individual changes, therefore the time complexity is quadratic + * (in reference to the strings' length). + * + * @param str1 the first string + * @param str2 the second string + * @return an integer representing the amount of atomic changes between {@code str1} and {@code str2} + */ + public static int calculate(String str1, String str2) { + int[][] matrix = new int[str1.length() + 1][str2.length() + 1]; + + for (int i = 0; i <= str1.length(); i++) { + for (int j = 0; j <= str2.length(); j++) { + if (i == 0) { // distance between "" and str2 == how long str2 is + matrix[i][j] = j; + } else if (j == 0) { // distance between str1 and "" == how long str1 is + matrix[i][j] = i; + } else { + int substitution = matrix[i - 1][j - 1] + + substitutionCost(str1.charAt(i - 1), str2.charAt(j - 1)); + int insertion = matrix[i][j - 1] + 1; + int deletion = matrix[i - 1][j] + 1; + + matrix[i][j] = min3(substitution, insertion, deletion); + } + } + } + + return matrix[str1.length()][str2.length()]; // result is in the bottom-right corner + } + + private static int substitutionCost(char a, char b) { + return (a == b) ? 0 : 1; + } + + private static int min3(int a, int b, int c) { + return Math.min(a, Math.min(b, c)); + } + } + + + public static class JunitStderrOutThief { + private final ByteArrayOutputStream out; + private final ByteArrayOutputStream err; + + private final PrintStream originalOut; + private final PrintStream originalErr; + + JunitStderrOutThief() { + out = new ByteArrayOutputStream(); + err = new ByteArrayOutputStream(); + originalOut = System.out; + originalErr = System.err; + } + + public void captureStreams(boolean capture) { + if (capture) { + out.reset(); + err.reset(); + } + + System.setOut(capture ? new PrintStream(out, true, StandardCharsets.UTF_8) : originalOut); + System.setErr(capture ? new PrintStream(err, true, StandardCharsets.UTF_8) : originalErr); + } + + private String get(ByteArrayOutputStream which) { + String string = which.toString(StandardCharsets.UTF_8); + which.reset(); + + return string; + } + + public String getOut() { + return get(out); + } + + public byte[] getOutBytes() { + byte[] bytes = out.toByteArray(); + out.reset(); + + return bytes; + } + + public String getErr() { + return get(err); + } + } + + + public static String readBinaryAsString(File f) throws IOException { + try (FileInputStream fis = new FileInputStream(f)) { + return readBinaryAsString(fis, "UTF-8", CodingErrorAction.REPLACE); + } + } + + public static String readBinaryAsString(FileInputStream input, String charBase, CodingErrorAction action) throws IOException { + CharsetDecoder decoder = Charset.forName(charBase).newDecoder(); + decoder.onMalformedInput(CodingErrorAction.IGNORE); + InputStreamReader reader = new InputStreamReader(input, decoder); + StringBuilder sb = new StringBuilder(); + while (true) { + int i = reader.read(); + if (i < 0) { + break; + } + sb.append((char) i); + } + reader.close(); + return sb.toString(); + } +} diff --git a/runtime-decompiler/src/test/java/org/jrd/backend/data/AbstractSourceTestClass.java b/runtime-decompiler/src/test/java/org/jrd/backend/data/AbstractSourceTestClass.java new file mode 100644 index 00000000..ee1ae569 --- /dev/null +++ b/runtime-decompiler/src/test/java/org/jrd/backend/data/AbstractSourceTestClass.java @@ -0,0 +1,278 @@ +package org.jrd.backend.data; + +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +abstract class AbstractSourceTestClass { + + private Process process; + private final String srcDir; + private final String targetDir; + private ProcessStdStreamReade sout; + private ProcessStdStreamReade serr; + + protected AbstractSourceTestClass() { + String tmpDir; + try { + tmpDir = Files.createDirectories(Path.of( + System.getProperty("java.io.tmpdir"), getClassName(), + "src", "main", "java", getPackageDirs() + )).toAbsolutePath().toString(); + } catch (IOException e) { + e.printStackTrace(); + tmpDir = System.getProperty("java.io.tmpdir"); + } + srcDir = tmpDir; + + try { + tmpDir = Files.createDirectories(Path.of( + System.getProperty("java.io.tmpdir"), getClassName(), "target" + )).toAbsolutePath().toString(); + } catch (IOException e) { + e.printStackTrace(); + tmpDir = System.getProperty("java.io.tmpdir"); + } + targetDir = tmpDir; + } + + abstract String getClassName(); + + abstract String getPackageName(); + + abstract String getGreetings(); + + abstract String getContentWithoutPackage(String nwHello); + + + public String getSrcDir() { + return srcDir; + } + + public String getTargetDir() { + return targetDir; + } + + protected String getPackageDirs() { + return getPackageName().replace('.', File.separatorChar); + } + + protected String getFqn() { + return getPackageName() + "." + getClassName(); + } + + + String getDotJavaPath() { + return getSrcDir() + File.separator + getClassName() + ".java"; + } + + String getDotClassPath() { + return getTargetDir() + File.separator + getPackageDirs() + File.separator + getClassName() + ".class"; + } + + String getClassRegex() { + return ".*" + getClassName() + ".*"; + } + + Pattern getJvmListRegex() { + return Pattern.compile( + String.format("^(\\d+).*%s.*$", getClassName()), + Pattern.MULTILINE + ); + } + + Pattern getExactClassRegex() { + return Pattern.compile( + String.format("^%s$", getFqn()), + Pattern.MULTILINE + ); + } + + AbstractSourceTestClass writeDefault() throws SourceTestClassWrapperException { + return write(getGreetings()); + } + + AbstractSourceTestClass write(String argument) throws SourceTestClassWrapperException { + try (PrintWriter pw = new PrintWriter(getDotJavaPath(), StandardCharsets.UTF_8)) { + pw.print(getContentWithPackage(argument)); + } catch (IOException e) { + throw new SourceTestClassWrapperException("Failed to write file '" + getDotJavaPath() + "'.", e); + } + + return this; + } + + AbstractSourceTestClass compile() throws SourceTestClassWrapperException { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + ByteArrayOutputStream errStream = new ByteArrayOutputStream(); + + int errLevel = compiler.run(null, null, errStream, + "-d", getTargetDir(), + getDotJavaPath() + ); + String errMessage = errStream.toString(StandardCharsets.UTF_8); + + if (errLevel != 0 || !errMessage.isEmpty()) { + throw new SourceTestClassWrapperException("Failed to compile file '" + getDotJavaPath() + ". Cause:\n" + errMessage); + } + + return this; + } + + AbstractSourceTestClass execute() throws SourceTestClassWrapperException { + ProcessBuilder pb = new ProcessBuilder("java", "-Djdk.attach.allowAttachSelf=true", getFqn()); + pb.directory(new File(getTargetDir())); + try { + process = pb.start(); + sout = new ProcessStdStreamReade(process.getInputStream()); + serr = new ProcessStdStreamReade(process.getErrorStream()); + new Thread(sout).start(); + new Thread(serr).start(); + } catch (IOException e) { + throw new SourceTestClassWrapperException("Failed to execute '" + getFqn() + "' class.", e); + } + + return this; + } + + String executeJavaP(String... options) throws SourceTestClassWrapperException, InterruptedException, IOException { + List commands = new ArrayList<>(); + commands.add("javap"); + commands.addAll(Arrays.asList(options)); + commands.add(getFqn()); + + ProcessBuilder pb = new ProcessBuilder(commands); + pb.redirectErrorStream(true); // merge process stderr with stdout + pb.directory(new File(getTargetDir())); + + Process javap; + try { + javap = pb.start(); + } catch (IOException e) { + throw new SourceTestClassWrapperException("Failed to execute javap on '" + getFqn() + "' class.", e); + } + + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(javap.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append(System.getProperty("line.separator", "\n")); + } + } + + javap.waitFor(); + return sb.toString(); + } + + void terminate() { + process.destroy(); + } + + boolean isAlive() { + return process.isAlive(); + } + + public String getPid() { + return Long.toString(process.pid()); + } + + public String getClasspath() { + return getTargetDir(); + } + + String getDefaultContentWithoutPackage() { + return getContentWithoutPackage(getGreetings()); + } + + String getDefaultContentWithPackage() { + return getContentWithPackage(getGreetings()); + } + + + String getContentWithPackage(String nwHello) { + return "package " + getPackageName() + ";\n\n" + + getContentWithoutPackage(nwHello); + } + + public String getEmptyClassWithPackage() { + return getEmptyClassWithPackage(getPackageName(), getClassName()); + } + + public static String getEmptyClassWithPackage(String pkg, String name) { + return "package " + pkg + ";\n" + + "public class " + name + " {}"; + } + + public byte[] getErrBytes() { + return serr.getBuffer(); + } + + public byte[] getOutBytes() { + return sout.getBuffer(); + } + + public String getErrString() { + return new String(serr.getBuffer(), StandardCharsets.UTF_8); + } + + public String getOutString() { + return new String(sout.getBuffer(), StandardCharsets.UTF_8); + } + + static class SourceTestClassWrapperException extends Exception { + SourceTestClassWrapperException(String message) { + super(message); + } + + SourceTestClassWrapperException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class ProcessStdStreamReade implements Runnable { + private final InputStream is; + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + boolean read = false; + + ProcessStdStreamReade(InputStream is) { + this.is = is; + } + + @Override + public void run() { + try { + int nRead; + byte[] data = new byte[8]; //slow, but we need quick updates + while ((nRead = is.readNBytes(data, 0, data.length)) != 0) { + buffer.write(data, 0, nRead); + } + buffer.flush(); + } catch (Exception ex) { + //ex.printStackTrace(); //yah, the process is being killed + } + read = true; + } + + public byte[] getBuffer() { + return buffer.toByteArray(); + } + + public boolean isRead() { + return read; + } + } +} diff --git a/runtime-decompiler/src/test/java/org/jrd/backend/data/CliTest.java b/runtime-decompiler/src/test/java/org/jrd/backend/data/CliTest.java index 9ba731b8..2c8c0a8d 100644 --- a/runtime-decompiler/src/test/java/org/jrd/backend/data/CliTest.java +++ b/runtime-decompiler/src/test/java/org/jrd/backend/data/CliTest.java @@ -1,11 +1,7 @@ package org.jrd.backend.data; -import org.jrd.backend.core.AgentRequestAction; import org.jrd.frontend.frame.main.DecompilationController; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.Timeout; @@ -15,11 +11,8 @@ import org.junit.jupiter.params.provider.ValueSource; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.PrintStream; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -28,9 +21,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -40,66 +31,20 @@ import static org.junit.jupiter.api.Assertions.*; @TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class CliTest { - private Model model; +public class CliTest extends AbstractAgentNeedingTest { private String[] args; private Cli cli; - private TestingDummyHelper dummy; + private static final String NEW_GREETINGS = "Greetings"; - private final StreamWrappers streams = new StreamWrappers(); private static final String UNKNOWN_FLAG = "--zyxwvutsrqponmlqjihgfedcba"; - @BeforeAll - static void startup() { - String agentPath = Config.getConfig().getAgentExpandedPath(); - - Assumptions.assumeTrue( - !agentPath.isEmpty(), - "Agent path is not set up, aborting CliTest." - ); - Assumptions.assumeTrue( - new File(agentPath).exists(), - "Agent path is set up to nonexistent file, aborting CliTest." - ); - } - - @Timeout(5) - @BeforeEach - void setup() throws InterruptedException { - try { - dummy = new TestingDummyHelper() - .write() - .compile() - .execute(); - } catch (TestingDummyHelper.TestingDummyException e) { - fail(e); - } - assertTrue(dummy.isAlive()); - - model = new Model(); // must be below dummy process execution to be aware of it during VmManager instantiation - while (model.getVmManager().findVmFromPidNoException(dummy.getPid()) == null) { - Thread.sleep(100); - model.getVmManager().updateLocalVMs(); - } - - streams.captureStreams(true); - } - - @AfterEach - void cleanup() { - streams.captureStreams(false); - - assertTrue(dummy.isAlive()); - // halt agent, otherwise an open socket prevents termination of dummy process - AgentRequestAction request = DecompilationController.createRequest( - model.getVmManager().findVmFromPid(dummy.getPid()), AgentRequestAction.RequestAction.HALT, "" - ); - String response = DecompilationController.submitRequest(model.getVmManager(), request); - assertEquals("ok", response); - - assertTrue(dummy.isAlive()); - dummy.terminate(); + @Override + AbstractSourceTestClass dummyProvider() throws AbstractSourceTestClass.SourceTestClassWrapperException { + return new TestingDummyHelper() + .writeDefault() + .compile() + .execute(); } private String prependSlashes(String original, int count) { @@ -121,22 +66,22 @@ private String twoSlashes(String flag) { @Test void testShouldBeVerbose() { // gui verbose - args = new String[] {VERBOSE}; + args = new String[]{VERBOSE}; cli = new Cli(args, model); assertTrue(cli.shouldBeVerbose()); // gui not verbose - args = new String[] {}; + args = new String[]{}; cli = new Cli(args, model); assertFalse(cli.shouldBeVerbose()); // cli verbose - args = new String[] {VERBOSE, UNKNOWN_FLAG}; + args = new String[]{VERBOSE, UNKNOWN_FLAG}; cli = new Cli(args, model); assertTrue(cli.shouldBeVerbose()); // cli not verbose - args = new String[] {UNKNOWN_FLAG}; + args = new String[]{UNKNOWN_FLAG}; cli = new Cli(args, model); assertFalse(cli.shouldBeVerbose()); } @@ -164,7 +109,7 @@ void testIsGui() { @Test void testHelp() throws Exception { - args = new String[] {HELP}; + args = new String[]{HELP}; cli = new Cli(args, model); cli.consumeCli(); @@ -191,9 +136,9 @@ private String processFormatDefault(String original) { return processFormat( original, dummy.getPid(), - new String[]{TestingDummyHelper.CLASS_REGEX}, - TestingDummyHelper.FQN, - TestingDummyHelper.DOT_CLASS_PATH, + new String[]{dummy.getClassRegex()}, + dummy.getFqn(), + dummy.getDotClassPath(), "javap" ); } @@ -285,7 +230,7 @@ private List queryJvmList() throws Exception { String jvms = streams.getOut(); List pids = new ArrayList<>(); // test dummy termination can take time to propagate => List.contains - Matcher m = TestingDummyHelper.JVM_LIST_REGEX.matcher(jvms); + Matcher m = dummy.getJvmListRegex().matcher(jvms); while (m.find()) { pids.add(m.group(1)); } @@ -310,9 +255,9 @@ void testListClasses(String pucComponent) throws Exception { cli.consumeCli(); String allClassesDefault = streams.getOut(); - Matcher m = TestingDummyHelper.EXACT_CLASS_REGEX.matcher(allClassesDefault); + Matcher m = dummy.getExactClassRegex().matcher(allClassesDefault); if (!m.find()) { - fail("Class " + TestingDummyHelper.CLASS_NAME + " not found when listing all classes."); + fail("Class " + dummy.getClassName() + " not found when listing all classes."); } // all regex @@ -322,9 +267,9 @@ void testListClasses(String pucComponent) throws Exception { cli.consumeCli(); String allClassesRegex = streams.getOut(); - m = TestingDummyHelper.EXACT_CLASS_REGEX.matcher(allClassesRegex); + m = dummy.getExactClassRegex().matcher(allClassesRegex); if (!m.find()) { - fail("Class " + TestingDummyHelper.CLASS_NAME + " not found when listing all classes via .* regex."); + fail("Class " + dummy.getClassName() + " not found when listing all classes via .* regex."); } assertEqualsWithTolerance( @@ -334,7 +279,7 @@ void testListClasses(String pucComponent) throws Exception { ); // exact class list differs between dummy process executions // specific regex - classListMatchesExactly(pucComponent, TestingDummyHelper.EXACT_CLASS_REGEX); + classListMatchesExactly(pucComponent, dummy.getExactClassRegex()); } @Test @@ -373,13 +318,13 @@ void testListPlugins() throws Exception { } void testBytes(String pucComponent) throws Exception { - args = new String[]{BYTES, pucComponent, TestingDummyHelper.CLASS_REGEX}; + args = new String[]{BYTES, pucComponent, dummy.getClassRegex()}; cli = new Cli(args, model); cli.consumeCli(); byte[] bytes = streams.getOutBytes(); - byte[] fileContents = Files.readAllBytes(Path.of(TestingDummyHelper.DOT_CLASS_PATH)); + byte[] fileContents = Files.readAllBytes(Path.of(dummy.getDotClassPath())); assertArrayEquals(fileContents, bytes); } @@ -391,13 +336,13 @@ void testBytes() throws Exception { } void testBase64Bytes(String pucComponent) throws Exception { - args = new String[]{BASE64, pucComponent, TestingDummyHelper.CLASS_REGEX}; + args = new String[]{BASE64, pucComponent, dummy.getClassRegex()}; cli = new Cli(args, model); cli.consumeCli(); byte[] base64Bytes = streams.getOut().trim().getBytes(StandardCharsets.UTF_8); - byte[] fileContents = Files.readAllBytes(Path.of(TestingDummyHelper.DOT_CLASS_PATH)); + byte[] fileContents = Files.readAllBytes(Path.of(dummy.getDotClassPath())); byte[] encoded = Base64.getEncoder().encode(fileContents); assertArrayEquals(encoded, base64Bytes); @@ -410,13 +355,13 @@ void testBase64Bytes() throws Exception { } void testBytesAndBase64BytesEqual(String pucComponent) throws Exception { - args = new String[]{BYTES, pucComponent, TestingDummyHelper.CLASS_REGEX}; + args = new String[]{BYTES, pucComponent, dummy.getClassRegex()}; cli = new Cli(args, model); cli.consumeCli(); byte[] bytes = streams.getOutBytes(); - args = new String[]{BASE64, pucComponent, TestingDummyHelper.CLASS_REGEX}; + args = new String[]{BASE64, pucComponent, dummy.getClassRegex()}; cli = new Cli(args, model); cli.consumeCli(); @@ -448,9 +393,9 @@ private Stream tooFewArgumentsSource() { new String[]{DECOMPILE, unimportantPid}, new String[]{DECOMPILE, unimportantPid, "javap"}, new String[]{COMPILE}, - new String[]{COMPILE, "-r"}, - new String[]{COMPILE, "-r", "-cp", unimportantPid}, - new String[]{COMPILE, "-r", "-cp", unimportantPid, "-p", "unimportantPluginName"} + new String[]{COMPILE, R}, + new String[]{COMPILE, R, CP, unimportantPid}, + new String[]{COMPILE, R, CP, unimportantPid, Cli.P, "unimportantPluginName"} ).map(a -> (Object) a).map(Arguments::of); // cast needed because of varargs factory method .of() } @@ -464,15 +409,15 @@ void testTooFewArguments(String[] wrongArgs) { } void testDecompileJavap(String pucComponent, String option) throws Exception { - args = new String[]{DECOMPILE, pucComponent, "javap" + option, TestingDummyHelper.CLASS_REGEX}; + args = new String[]{DECOMPILE, pucComponent, "javap" + option, dummy.getClassRegex()}; cli = new Cli(args, model); cli.consumeCli(); - String jrdDisassembled = streams.getOut(); - String javapDisassembled = TestingDummyHelper.executeJavaP(option); + String jrdDisassembled = streams.getOut().trim(); + String javapDisassembled = dummy.executeJavaP(option).trim(); // JRD javap has additional debug comment lines + header is different - assertEqualsWithTolerance(jrdDisassembled, javapDisassembled, 0.8); + assertEqualsWithTolerance(jrdDisassembled, javapDisassembled, 0.9); } @ParameterizedTest @@ -484,7 +429,7 @@ void testDecompileJavap(String option) throws Exception { @Test void testDecompileUnknownPlugin() { - args = new String[]{DECOMPILE, dummy.getPid(), UNKNOWN_FLAG, TestingDummyHelper.CLASS_REGEX}; + args = new String[]{DECOMPILE, dummy.getPid(), UNKNOWN_FLAG, dummy.getClassRegex()}; cli = new Cli(args, model); assertThrows(RuntimeException.class, () -> cli.consumeCli()); @@ -499,7 +444,7 @@ void testDecompileFilePlugin() { fail(e); } - args = new String[]{DECOMPILE, dummy.getPid(), emptyFilePlugin, TestingDummyHelper.CLASS_REGEX}; + args = new String[]{DECOMPILE, dummy.getPid(), emptyFilePlugin, dummy.getClassRegex()}; cli = new Cli(args, model); assertThrows(RuntimeException.class, () -> cli.consumeCli()); @@ -530,14 +475,13 @@ void testArgumentCleaning() throws Exception { } void testOverwrite(String pucComponent) throws Exception { - String newGreeting = "Greetings"; - createReplacement(newGreeting); + createReplacement(NEW_GREETINGS); args = new String[]{ OVERWRITE, pucComponent, - TestingDummyHelper.FQN, - TestingDummyHelper.DOT_CLASS_PATH // contains newGreeting because of try-catch above + dummy.getFqn(), + dummy.getDotClassPath() // contains newGreeting because of try-catch above }; cli = new Cli(args, model); @@ -545,70 +489,107 @@ void testOverwrite(String pucComponent) throws Exception { assertTrue(streams.getOut().contains("success")); // assert that change propagated, unfortunately we have to rely on another operation here - bytecodeContainsNewString(pucComponent, newGreeting); + bytecodeContainsNewString(pucComponent, NEW_GREETINGS); } + + /** + * Why this is passing: + * The TestingDummy never request new class definition. + * So yes, we change the bytecode, and that get changed. + * Then we can see the modified definition + * + * @throws Exception + */ @Test - void testOverwrite() throws Exception { + void testOverwriteRunning() throws Exception { testOverwrite(dummy.getPid()); + dummyOutputWasNotChanged(NEW_GREETINGS); + } + + @Test + void testOverwriteCP() throws Exception { testOverwrite(dummy.getClasspath()); } void testOverwriteStdIn(String pucComponent) throws Exception { - String newGreeting = "Greetings"; - createReplacement(newGreeting); + createReplacement(NEW_GREETINGS); args = new String[]{ OVERWRITE, pucComponent, - TestingDummyHelper.FQN + dummy.getFqn() }; cli = new Cli(args, model); // setup input stream - ByteArrayInputStream fakeIn = new ByteArrayInputStream(Files.readAllBytes(Path.of(TestingDummyHelper.DOT_CLASS_PATH))); + ByteArrayInputStream fakeIn = new ByteArrayInputStream(Files.readAllBytes(Path.of(dummy.getDotClassPath()))); final InputStream originalIn = System.in; System.setIn(fakeIn); assertDoesNotThrow(() -> cli.consumeCli()); assertTrue(streams.getOut().contains("success")); - bytecodeContainsNewString(pucComponent, newGreeting); + bytecodeContainsNewString(pucComponent, NEW_GREETINGS); System.setIn(originalIn); // revert input stream } + /** + * Why this is passing: + * + * @throws Exception + */ @Test - void testOverwriteStdIn() throws Exception { + void testOverwriteStdInRunning() throws Exception { testOverwriteStdIn(dummy.getPid()); + dummyOutputWasNotChanged(NEW_GREETINGS); + } + + @Test + void testOverwriteStdInCp() throws Exception { testOverwriteStdIn(dummy.getClasspath()); } - private void createReplacement(String newGreeting) { + private static void createReplacement(String newGreeting) { try { new TestingDummyHelper() .write(newGreeting) .compile(); - } catch (TestingDummyHelper.TestingDummyException e) { + } catch (AbstractSourceTestClass.SourceTestClassWrapperException e) { fail("Failed to create data to be uploaded.", e); } } + /** + * Although the class definition was transforemd, in OpenJDK HotSpot + * this implementation of dummy do not request the new definition, + * adn continues to spit out its jitted output + * + * @param newString + * @throws Exception + */ + private void dummyOutputWasNotChanged(String newString) throws Exception { + Thread.sleep(1000); //the test must wait while reader do something + Assertions.assertTrue(dummy.getOutString().contains(dummy.getGreetings())); + Assertions.assertFalse(dummy.getOutString().contains(newString)); + } + private void bytecodeContainsNewString(String pucComponent, String newString) throws Exception { - args = new String[]{DECOMPILE, pucComponent, "javap-v", TestingDummyHelper.CLASS_REGEX}; + args = new String[]{DECOMPILE, pucComponent, "javap-v", dummy.getClassRegex()}; cli = new Cli(args, model); cli.consumeCli(); String overwrittenClassInVm = streams.getOut(); - assertFalse(overwrittenClassInVm.contains(TestingDummyHelper.DEFAULT_GREETING)); + assertFalse(overwrittenClassInVm.contains(dummy.getGreetings())); assertTrue(overwrittenClassInVm.contains(newString)); } @Test void testOverwriteWarning() { - String nonClassFile = TestingDummyHelper.DOT_CLASS_PATH.replace(".class", ""); + String nonClassFile = dummy.getDotClassPath().replace(".class", ""); try { - Files.copy(Path.of(TestingDummyHelper.DOT_CLASS_PATH), Path.of(nonClassFile), StandardCopyOption.REPLACE_EXISTING); + Files.copy(Path.of(dummy.getDotClassPath()), Path.of(nonClassFile), StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { fail("Failed to copy file.", e); } @@ -616,7 +597,7 @@ void testOverwriteWarning() { args = new String[]{ OVERWRITE, dummy.getPid(), - TestingDummyHelper.FQN, + dummy.getFqn(), nonClassFile }; cli = new Cli(args, model); @@ -631,8 +612,8 @@ void testOverwriteError() { args = new String[]{ OVERWRITE, dummy.getPid(), - TestingDummyHelper.FQN, - TestingDummyHelper.TARGET_DIR + dummy.getFqn(), + dummy.getTargetDir() }; cli = new Cli(args, model); @@ -647,7 +628,7 @@ void testOverwriteAgentError() { OVERWRITE, dummy.getPid(), UNKNOWN_FLAG, // non-FQN makes agent not find the class - TestingDummyHelper.DOT_CLASS_PATH + dummy.getDotClassPath() }; cli = new Cli(args, model); @@ -683,24 +664,29 @@ void testInitAgentError() { assertThrows(RuntimeException.class, () -> cli.consumeCli()); } - private Stream incorrectClassContents() { - return Stream.of( - TestingDummyHelper.getDefaultContent(), // no package - "package " + TestingDummyHelper.PACKAGE_NAME + ";", // no class - "uncompilable text?" - ).map(s -> s.getBytes(StandardCharsets.UTF_8)); + @Test + public void testGuessNameIncorrectNoPkg() { + byte[] contents = dummy.getDefaultContentWithoutPackage().getBytes(StandardCharsets.UTF_8); + assertThrows(RuntimeException.class, () -> Cli.guessName(contents)); } - @ParameterizedTest(name = "[{index}]") - @MethodSource("incorrectClassContents") - void testGuessNameIncorrect(byte[] contents) { + @Test + public void testGuessNameIncorrectNoClass() { + byte[] contents = ("package " + dummy.getPackageName() + ";").getBytes(StandardCharsets.UTF_8); + assertThrows(RuntimeException.class, () -> Cli.guessName(contents)); + } + + @Test + public void testGuessNameIncorrectNoCompile() { + byte[] contents = "uncompilable text?".getBytes(StandardCharsets.UTF_8); assertThrows(RuntimeException.class, () -> Cli.guessName(contents)); } + private Stream correctClassContents() { return Stream.of( - TestingDummyHelper.getDefaultContentWithPackage(), - TestingDummyHelper.getEmptyClass() + new TestingDummyHelper().getDefaultContentWithPackage(), + new TestingDummyHelper().getEmptyClassWithPackage() ).map(s -> s.getBytes(StandardCharsets.UTF_8)); } @@ -709,7 +695,7 @@ private Stream correctClassContents() { void testGuessNameCorrect(byte[] contents) { try { assertEquals( - TestingDummyHelper.PACKAGE_NAME + "." + TestingDummyHelper.CLASS_NAME, + dummy.getPackageName() + "." + dummy.getClassName(), Cli.guessName(contents) ); } catch (IOException e) { @@ -717,120 +703,4 @@ void testGuessNameCorrect(byte[] contents) { } } - private static boolean isDifferenceTolerable(double samenessPercentage, int actualChanges, int totalSize) { - assert samenessPercentage >= 0 && samenessPercentage <= 1.0; - - double changesAllowed = (1.0 - samenessPercentage) * totalSize; - return actualChanges <= changesAllowed; - } - - static void assertEqualsWithTolerance(String s1, String s2, double samenessPercentage) { - assertTrue(isDifferenceTolerable( - samenessPercentage, - LevenshteinDistance.calculate(s1, s2), - Math.max(s1.length(), s2.length()) - )); - } - - static void assertEqualsWithTolerance(List l1, List l2, double samenessPercentage) { - // symmetric difference - Set intersection = new HashSet<>(l1); - intersection.retainAll(l2); - - Set difference = new HashSet<>(); - difference.addAll(l1); - difference.addAll(l2); - difference.removeAll(intersection); - - assertTrue(isDifferenceTolerable(samenessPercentage, difference.size(), Math.max(l1.size(), l2.size()))); - } - - private static final class LevenshteinDistance { - /** - * Calculates the Levenshtein distance between two strings.
- * Uses a 2D array to represent individual changes, therefore the time complexity is quadratic - * (in reference to the strings' length). - * @param str1 the first string - * @param str2 the second string - * @return an integer representing the amount of atomic changes between {@code str1} and {@code str2} - */ - public static int calculate(String str1, String str2) { - int[][] matrix = new int[str1.length() + 1][str2.length() + 1]; - - for (int i = 0; i <= str1.length(); i++) { - for (int j = 0; j <= str2.length(); j++) { - if (i == 0) { // distance between "" and str2 == how long str2 is - matrix[i][j] = j; - } else if (j == 0) { // distance between str1 and "" == how long str1 is - matrix[i][j] = i; - } else { - int substitution = matrix[i - 1][j - 1] + - substitutionCost(str1.charAt(i - 1), str2.charAt(j - 1)); - int insertion = matrix[i][j - 1] + 1; - int deletion = matrix[i - 1][j] + 1; - - matrix[i][j] = min3(substitution, insertion, deletion); - } - } - } - - return matrix[str1.length()][str2.length()]; // result is in the bottom-right corner - } - - private static int substitutionCost(char a, char b) { - return (a == b) ? 0 : 1; - } - - private static int min3(int a, int b, int c) { - return Math.min(a, Math.min(b, c)); - } - } - - - private static class StreamWrappers { - private final ByteArrayOutputStream out; - private final ByteArrayOutputStream err; - - private final PrintStream originalOut; - private final PrintStream originalErr; - - StreamWrappers() { - out = new ByteArrayOutputStream(); - err = new ByteArrayOutputStream(); - originalOut = System.out; - originalErr = System.err; - } - - public void captureStreams(boolean capture) { - if (capture) { - out.reset(); - err.reset(); - } - - System.setOut(capture ? new PrintStream(out, true, StandardCharsets.UTF_8) : originalOut); - System.setErr(capture ? new PrintStream(err, true, StandardCharsets.UTF_8) : originalErr); - } - - private String get(ByteArrayOutputStream which) { - String string = which.toString(StandardCharsets.UTF_8); - which.reset(); - - return string; - } - - public String getOut() { - return get(out); - } - - public byte[] getOutBytes() { - byte[] bytes = out.toByteArray(); - out.reset(); - - return bytes; - } - - public String getErr() { - return get(err); - } - } } diff --git a/runtime-decompiler/src/test/java/org/jrd/backend/data/CompileUploadCliTest.java b/runtime-decompiler/src/test/java/org/jrd/backend/data/CompileUploadCliTest.java new file mode 100644 index 00000000..74c05e92 --- /dev/null +++ b/runtime-decompiler/src/test/java/org/jrd/backend/data/CompileUploadCliTest.java @@ -0,0 +1,406 @@ +package org.jrd.backend.data; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Base64; +import java.util.stream.Collectors; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class CompileUploadCliTest extends AbstractAgentNeedingTest { + private static final String NEW_GREETING = "Greetings"; + + + @Override + AbstractSourceTestClass dummyProvider() throws AbstractSourceTestClass.SourceTestClassWrapperException { + return new ModifiableDummyTestingHelper() + .writeDefault() + .compile() + .execute(); + } + + void testBytes(String pucComponent) throws Exception { + String[] args = new String[]{Cli.BYTES, pucComponent, dummy.getClassRegex()}; + Cli cli = new Cli(args, model); + + cli.consumeCli(); + + byte[] bytes = streams.getOutBytes(); + byte[] fileContents = Files.readAllBytes(Path.of(dummy.getDotClassPath())); + + Assertions.assertArrayEquals(fileContents, bytes); + } + + + @Test + void testBytes() throws Exception { + testBytes(dummy.getPid()); + testBytes(dummy.getClasspath()); + } + + void testBase64Bytes(String pucComponent) throws Exception { + String[] args = new String[]{Cli.BASE64, pucComponent, dummy.getClassRegex()}; + Cli cli = new Cli(args, model); + + cli.consumeCli(); + + byte[] base64Bytes = streams.getOut().trim().getBytes(StandardCharsets.UTF_8); + byte[] fileContents = Files.readAllBytes(Path.of(dummy.getDotClassPath())); + byte[] encoded = Base64.getEncoder().encode(fileContents); + + Assertions.assertArrayEquals(encoded, base64Bytes); + } + + @Test + void testBase64Bytes() throws Exception { + testBase64Bytes(dummy.getPid()); + testBase64Bytes(dummy.getClasspath()); + } + + void testBytesAndBase64BytesEqual(String pucComponent) throws Exception { + String[] args = new String[]{Cli.BYTES, pucComponent, dummy.getClassRegex()}; + Cli cli = new Cli(args, model); + + cli.consumeCli(); + byte[] bytes = streams.getOutBytes(); + + args = new String[]{Cli.BASE64, pucComponent, dummy.getClassRegex()}; + cli = new Cli(args, model); + + cli.consumeCli(); + String base64 = streams.getOut().trim(); + byte[] decoded = Base64.getDecoder().decode(base64); + + Assertions.assertArrayEquals(bytes, decoded); + } + + @Test + void testBytesAndBase64BytesEqual() throws Exception { + testBytesAndBase64BytesEqual(dummy.getPid()); + testBytesAndBase64BytesEqual(dummy.getClasspath()); + } + + + void testDecompileJavap(String pucComponent, String option) throws Exception { + String[] args = new String[]{Cli.DECOMPILE, pucComponent, "javap" + option, dummy.getClassRegex()}; + Cli cli = new Cli(args, model); + + cli.consumeCli(); + String jrdDisassembled = streams.getOut().trim(); + String javapDisassembled = dummy.executeJavaP(option).trim(); + + // JRD javap has additional debug comment lines + header is different + assertEqualsWithTolerance(jrdDisassembled, javapDisassembled, 0.9); + } + + @ParameterizedTest + @ValueSource(strings = {"", "-v"}) + void testDecompileJavap(String option) throws Exception { + testDecompileJavap(dummy.getPid(), option); + testDecompileJavap(dummy.getClasspath(), option); + } + + + void testOverwrite(String pucComponent) throws Exception { + createReplacement(NEW_GREETING); + Assertions.assertDoesNotThrow(() -> overwrite(dummy, model, dummy.getDotClassPath())); + Assertions.assertTrue(streams.getOut().contains("success")); + + // assert that change propagated, unfortunately we have to rely on another operation here + bytecodeContainsNewString(pucComponent, NEW_GREETING); + } + + @Test + void testOverwriteRunning() throws Exception { + testOverwrite(dummy.getPid()); + dummyOutputWasChanged(NEW_GREETING); + } + + @Test + void testOverwriteCp() throws Exception { + testOverwrite(dummy.getClasspath()); + } + + void testOverwriteStdIn(String pucComponent) throws Exception { + createReplacement(NEW_GREETING); + + String[] args = new String[]{ + Cli.OVERWRITE, + pucComponent, + dummy.getFqn() + }; + Cli cli = new Cli(args, model); + + // setup input stream + ByteArrayInputStream fakeIn = new ByteArrayInputStream(Files.readAllBytes(Path.of(dummy.getDotClassPath()))); + final InputStream originalIn = System.in; + System.setIn(fakeIn); + + Assertions.assertDoesNotThrow(() -> cli.consumeCli()); + Assertions.assertTrue(streams.getOut().contains("success")); + bytecodeContainsNewString(pucComponent, NEW_GREETING); + System.setIn(originalIn); // revert input stream + } + + /** + * Why this have oposite definition then its soulmate in CliTest: + *

+ * Because TestingModifiableDummy keeps asking for new class definition + * So immediately once we disconnect agent, the class get restored for original receipt + */ + @Test + void testOverwriteStdInRunning() throws Exception { + testOverwriteStdIn(dummy.getPid()); + dummyOutputWasChanged(NEW_GREETING); + } + + @Test + void testOverwriteStdInCp() throws Exception { + testOverwriteStdIn(dummy.getClasspath()); + } + + private static void createReplacement(String newGreeting) { + try { + new ModifiableDummyTestingHelper() + .write(newGreeting) + .compile(); + } catch (AbstractSourceTestClass.SourceTestClassWrapperException e) { + Assertions.fail("Failed to create data to be uploaded.", e); + } + } + + /** + * As the class definition was transforemd, in OpenJDK HotSpot + * this implementation of dummy did requested (by the call of new ..().print() ) + * this new deffinition and so we can see the change + * + * @param newString + * @throws Exception + */ + private void dummyOutputWasChanged(String newString) throws Exception { + Thread.sleep(1000); //the test must wait while reader do something + Assertions.assertTrue(dummy.getOutString().contains(dummy.getGreetings())); + Assertions.assertTrue(dummy.getOutString().contains(newString)); + } + + private void bytecodeContainsNewString(String pucComponent, String newString) throws Exception { + String[] args = new String[]{Cli.DECOMPILE, pucComponent, "javap-v", dummy.getClassRegex()}; + Cli cli = new Cli(args, model); + + cli.consumeCli(); + String overwrittenClassInVm = streams.getOut(); + + Assertions.assertFalse(overwrittenClassInVm.contains(dummy.getGreetings())); + Assertions.assertTrue(overwrittenClassInVm.contains(newString)); + } + + @Test + void testOverwriteWarning() { + String nonClassFile = dummy.getDotClassPath().replace(".class", ""); + try { + Files.copy(Path.of(dummy.getDotClassPath()), Path.of(nonClassFile), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + Assertions.fail("Failed to copy file.", e); + } + Assertions.assertDoesNotThrow(() -> overwrite(dummy, model, nonClassFile)); + String output = streams.getErr(); + Assertions.assertTrue(output.contains("WARNING:")); + } + + public static boolean pluginExists(String plugin, Model model) throws Exception { + return model.getPluginManager() + .getWrappers() + .stream() + .anyMatch(wrapper -> wrapper.getName().equals(plugin)); + } + + @Test + void testDecompileCompileCfr() throws Exception { + final String plugin = "Cfr"; + Assumptions.assumeTrue(pluginExists(plugin, model), "plugin: " + plugin + " not available"); + File decompiledFile = decompile(plugin, dummy, model); + String sOrig = Files.readAllLines( + decompiledFile.toPath(), StandardCharsets.UTF_8).stream() + .collect(Collectors.joining("\n")); + String sNoCommnets = Files.readAllLines( + decompiledFile.toPath(), StandardCharsets.UTF_8).stream() + .filter(a -> !(a.trim().startsWith("/") || a.trim().startsWith("*"))) + .collect(Collectors.joining("\n")); + assertEqualsWithTolerance(sOrig, sNoCommnets, 0.9); + assertEqualsWithTolerance(sNoCommnets, dummy.getDefaultContentWithPackage(), 0.85); + + File compiledFile = compile(null, dummy, model, decompiledFile); + String compiled = readBinaryAsString(compiledFile); + String original = readBinaryAsString(new File(dummy.getDotClassPath())); + assertEqualsWithTolerance(compiled, original, 0.9); + + Assertions.assertDoesNotThrow(() -> overwrite(dummy, model, compiledFile)); + Assertions.assertThrows(Exception.class, () -> overwrite(dummy, model, decompiledFile)); //src instead of bin == nonsense + } + + private static void overwrite(AbstractSourceTestClass dummy, Model model, String bin) throws Exception { + overwrite(dummy, model, new File(bin)); + } + + private static void overwrite(AbstractSourceTestClass dummy, Model model, File bin) throws Exception { + String[] args = new String[]{ + Cli.OVERWRITE, + dummy.getPid(), + dummy.getFqn(), + bin.getAbsolutePath() + }; + Cli cli = new Cli(args, model); + cli.consumeCli(); + } + + private static File compile(String plugin, AbstractSourceTestClass dummy, Model model, File src) throws Exception { + File compiledFile = File.createTempFile("jrd", "test.class"); + String[] args; + if (plugin != null) { + args = new String[]{ + Cli.COMPILE, + Cli.P, plugin, + Cli.CP, dummy.getPid(), + src.getAbsolutePath(), + Cli.SAVE_LIKE, Cli.Saving.EXACT, + Cli.SAVE_AS, compiledFile.getAbsolutePath() + }; + } else { + args = new String[]{ + Cli.COMPILE, + Cli.CP, dummy.getPid(), + src.getAbsolutePath(), + Cli.SAVE_LIKE, Cli.Saving.EXACT, + Cli.SAVE_AS, compiledFile.getAbsolutePath() + }; + } + Cli cli = new Cli(args, model); + cli.consumeCli(); + return compiledFile; + } + + private static File decompile(String plugin, AbstractSourceTestClass dummy, Model model) throws Exception { + File decompiledFile = File.createTempFile("jrd", "test.java"); + String[] args = new String[]{ + Cli.DECOMPILE, + dummy.getPid(), + plugin, + dummy.getFqn(), + Cli.SAVE_LIKE, Cli.Saving.EXACT, + Cli.SAVE_AS, decompiledFile.getAbsolutePath() + }; + Cli cli = new Cli(args, model); + cli.consumeCli(); + return decompiledFile; + } + + @Test + void testDecompileCompileJasm() throws Exception { + final String plugin = "jasm"; + Assumptions.assumeTrue(pluginExists(plugin, model), "plugin: " + plugin + " not available"); + File decompiledFile = decompile(plugin, dummy, model); + String sOrig = Files.readAllLines(decompiledFile.toPath(), StandardCharsets.UTF_8).stream().collect(Collectors.joining("\n")); + String sLine = Files.readAllLines(decompiledFile.toPath(), StandardCharsets.UTF_8).stream().collect(Collectors.joining(" ")); + //unluckily there is nothing to compare to, unless we wish to call jasm from here "again" + //so at least some verifiers + Assertions.assertTrue(sOrig.contains("{")); + Assertions.assertTrue(sOrig.contains("}")); + Assertions.assertTrue(sOrig.contains("version")); + Assertions.assertTrue(sOrig.contains("invokevirtual")); + Assertions.assertTrue(sOrig.contains("invokestatic")); + Assertions.assertTrue(sOrig.contains("goto")); + Assertions.assertTrue(sLine.matches(".*package\\s+testing/modifiabledummy;.*")); + Assertions.assertTrue(sLine.matches(".*class\\s+TestingModifiableDummy.*")); + Assertions.assertTrue(sLine.matches(".*public\\s+Method\\s+\"\".*")); + Assertions.assertTrue(sLine.matches(".*new\\s+class\\s+TestingModifiableDummy.*")); + Assertions.assertTrue(sLine.matches(".*private\\s+Method\\s+print.*")); + Assertions.assertTrue(sLine.matches(".*getstatic\\s+Field\\s+java/lang/System.out:\"Ljava/io/PrintStream;\";.*")); + Assertions.assertTrue(sLine.matches(".*ldc\\s+String\\s+\"Hello\";.*")); + + File compiledFile = compile(plugin, dummy, model, decompiledFile); + String compiled = readBinaryAsString(compiledFile); + String original = readBinaryAsString(new File(dummy.getDotClassPath())); + assertEqualsWithTolerance(compiled, original, 0.4); //yah, jasm performance is not great + + Assertions.assertDoesNotThrow(() -> overwrite(dummy, model, compiledFile)); + Assertions.assertThrows(Exception.class, () -> overwrite(dummy, model, decompiledFile)); //src instead of bin == nonsense + } + + @Test + void testDecompileCompileJcoder() throws Exception { + final String plugin = "jcoder"; + Assumptions.assumeTrue(pluginExists(plugin, model), "plugin: " + plugin + " not available"); + File decompiledFile = decompile(plugin, dummy, model); + String sOrig = Files.readAllLines(decompiledFile.toPath(), StandardCharsets.UTF_8).stream().collect(Collectors.joining("\n")); + //unluckily there is nothing to compare to, unless we wish to call jcoder from here "again" + Assertions.assertTrue(sOrig.length() > 1000); + + File compiledFile = compile(plugin, dummy, model, decompiledFile); + String compiled = readBinaryAsString(compiledFile); + String original = readBinaryAsString(new File(dummy.getDotClassPath())); + assertEqualsWithTolerance(compiled, original, 0.4); //yah, jasm performance is not greate + + Assertions.assertDoesNotThrow(() -> overwrite(dummy, model, compiledFile)); + Assertions.assertThrows(Exception.class, () -> overwrite(dummy, model, decompiledFile)); //src instead of bin == nonsense + } + + + @Test + void testGlobalApi() throws Exception { + String[] args = new String[]{ + Cli.API, + dummy.getPid() + }; + Cli cli = new Cli(args, model); + cli.consumeCli(); + String apiHelp = streams.getOut(); + Assertions.assertTrue(apiHelp.contains("org.jrd.agent.api.Variables.Global.get")); + Assertions.assertTrue(apiHelp.contains("org.jrd.agent.api.Variables.Global.set")); + + File decompiledFile = File.createTempFile("jrd", "test.java"); + + String withNonsense = dummy.getDefaultContentWithPackage().replace("/*API_PLACEHOLDER*/", "some nonsese\n"); + Files.write(decompiledFile.toPath(), withNonsense.getBytes(StandardCharsets.UTF_8)); + Exception expectedEx = null; + try { + File compiledFile = compile(null, dummy, model, decompiledFile); + } catch (Exception ex) { + String afterCompilationsOut = streams.getOut(); + String afterCompilationsErr = streams.getErr(); + expectedEx = ex; + } + Assertions.assertNotNull(expectedEx); + + String withApi = dummy.getDefaultContentWithPackage().replace("/*API_PLACEHOLDER*/", "" + + "Integer i = (Integer)(org.jrd.agent.api.Variables.Global.getOrCreate(\"counter\", new Integer(0)));\n" + + "i=i+1;\n" + + "org.jrd.agent.api.Variables.Global.set(\"counter\", i);\n" + + "System.out.println(\"API: \"+i+\" had spoken\");\n"); + Files.write(decompiledFile.toPath(), withApi.getBytes(StandardCharsets.UTF_8)); + File compiledFile = compile(null, dummy, model, decompiledFile); + String compiled = readBinaryAsString(compiledFile); + String original = readBinaryAsString(new File(dummy.getDotClassPath())); + assertEqualsWithTolerance(compiled, original, 0.4); + + Assertions.assertDoesNotThrow(() -> overwrite(dummy, model, compiledFile)); + + Thread.sleep(1000); + String mainOutput = dummy.getOutString(); + Assertions.assertTrue(mainOutput.contains("API: 1 had spoken")); + Assertions.assertTrue(mainOutput.contains("API: 2 had spoken")); + Assertions.assertTrue(mainOutput.contains("API: 3 had spoken")); + Assertions.assertTrue(mainOutput.contains("API: 4 had spoken")); + } +} diff --git a/runtime-decompiler/src/test/java/org/jrd/backend/data/ModifiableDummyTestingHelper.java b/runtime-decompiler/src/test/java/org/jrd/backend/data/ModifiableDummyTestingHelper.java new file mode 100644 index 00000000..753b3ddd --- /dev/null +++ b/runtime-decompiler/src/test/java/org/jrd/backend/data/ModifiableDummyTestingHelper.java @@ -0,0 +1,34 @@ +package org.jrd.backend.data; + +class ModifiableDummyTestingHelper extends AbstractSourceTestClass { + + @Override + protected String getClassName() { + return "TestingModifiableDummy"; + } + + @Override + protected String getPackageName() { + return "testing.modifiabledummy"; + } + + @Override + String getContentWithoutPackage(String nwHello) { + return "public class " + getClassName() + " {\n" + + " public static void main(String[] args) throws InterruptedException {\n" + + " while(true) {\n" + + " new " + getClassName() + "().print();\n" + + " Thread.sleep(100);\n" + + " }\n" + + " }\n" + + " private void print(){\n/*API_PLACEHOLDER*/\nSystem.out.println(\"" + nwHello + "\");}\n" + + "}\n"; + } + + @Override + String getGreetings() { + return "Hello"; + } + + +} diff --git a/runtime-decompiler/src/test/java/org/jrd/backend/data/TestingDummyHelper.java b/runtime-decompiler/src/test/java/org/jrd/backend/data/TestingDummyHelper.java index b075cd5d..c891f554 100644 --- a/runtime-decompiler/src/test/java/org/jrd/backend/data/TestingDummyHelper.java +++ b/runtime-decompiler/src/test/java/org/jrd/backend/data/TestingDummyHelper.java @@ -1,197 +1,33 @@ package org.jrd.backend.data; -import javax.tools.JavaCompiler; -import javax.tools.ToolProvider; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Pattern; -import java.util.stream.Collectors; +class TestingDummyHelper extends AbstractSourceTestClass { -class TestingDummyHelper { - - private Process process; - - static final String CLASS_NAME = "TestingDummy"; - static final String PACKAGE_NAME = "testing.dummy"; - static final String PACKAGE_DIRS = PACKAGE_NAME.replace('.', File.separatorChar); - static final String FQN = PACKAGE_NAME + "." + CLASS_NAME; - - static final String SRC_DIR; - static final String TARGET_DIR; - - static { - String tmpDir; - try { - tmpDir = Files.createDirectories(Path.of( - System.getProperty("java.io.tmpdir"), CLASS_NAME, - "src", "main", "java", PACKAGE_DIRS - )).toAbsolutePath().toString(); - } catch (IOException e) { - e.printStackTrace(); - tmpDir = System.getProperty("java.io.tmpdir"); - } - SRC_DIR = tmpDir; - - try { - tmpDir = Files.createDirectories(Path.of( - System.getProperty("java.io.tmpdir"), CLASS_NAME, "target" - )).toAbsolutePath().toString(); - } catch (IOException e) { - e.printStackTrace(); - tmpDir = System.getProperty("java.io.tmpdir"); - } - TARGET_DIR = tmpDir; + @Override + protected String getClassName() { + return "TestingDummy"; } - static final String DOT_JAVA_PATH = SRC_DIR + File.separator + CLASS_NAME + ".java"; - static final String DOT_CLASS_PATH = TARGET_DIR + File.separator + PACKAGE_DIRS + File.separator + CLASS_NAME + ".class"; - - static final String CLASS_REGEX = ".*" + CLASS_NAME + ".*"; - static final Pattern JVM_LIST_REGEX = Pattern.compile( - String.format("^(\\d+).*%s.*$", CLASS_NAME), - Pattern.MULTILINE - ); - static final Pattern EXACT_CLASS_REGEX = Pattern.compile( - String.format("^%s$", FQN), - Pattern.MULTILINE - ); - - static final String DEFAULT_GREETING = "Hello"; - - - TestingDummyHelper write(String greeting) throws TestingDummyException { - try (PrintWriter pw = new PrintWriter(DOT_JAVA_PATH, StandardCharsets.UTF_8)) { - pw.print(getContentWithPackage(greeting)); - } catch (IOException e) { - throw new TestingDummyException("Failed to write file '" + DOT_JAVA_PATH + "'.", e); - } - - return this; - } - - TestingDummyHelper write() throws TestingDummyException { - return write("Hello"); - } - - TestingDummyHelper compile() throws TestingDummyException { - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - ByteArrayOutputStream errStream = new ByteArrayOutputStream(); - - int errLevel = compiler.run(null, null, errStream, - "-d", TARGET_DIR, - DOT_JAVA_PATH - ); - String errMessage = errStream.toString(StandardCharsets.UTF_8); - - if (errLevel != 0 || !errMessage.isEmpty()) { - throw new TestingDummyException("Failed to compile file '" + DOT_JAVA_PATH + ". Cause:\n" + errMessage); - } - - return this; - } - - TestingDummyHelper execute() throws TestingDummyException { - ProcessBuilder pb = new ProcessBuilder("java", "-Djdk.attach.allowAttachSelf=true", FQN); - pb.directory(new File(TARGET_DIR)); - - try { - process = pb.start(); - } catch (IOException e) { - throw new TestingDummyException("Failed to execute '" + FQN + "' class.", e); - } - - return this; + @Override + protected String getPackageName() { + return "testing.dummy"; } - static String executeJavaP(String... options) throws TestingDummyException, InterruptedException, IOException { - List commands = new ArrayList<>(); - commands.add("javap"); - commands.addAll(Arrays.asList(options)); - commands.add(FQN); - - ProcessBuilder pb = new ProcessBuilder(commands); - pb.directory(new File(TARGET_DIR)); - - Process javap; - try { - javap = pb.start(); - } catch (IOException e) { - throw new TestingDummyException("Failed to execute javap on '" + FQN + "' class.", e); - } - javap.waitFor(); - - try (BufferedReader br = new BufferedReader(new InputStreamReader( - javap.getInputStream(), StandardCharsets.UTF_8 - ))) { - String output = br.lines().collect(Collectors.joining("\n")); - javap.destroy(); - return output; - } - } - - void terminate() { - process.destroy(); - } - - boolean isAlive() { - return process.isAlive(); - } - - String getPid() { - return Long.toString(process.pid()); - } - - String getClasspath() { - return TARGET_DIR; - } - - static String getDefaultContent() { - return getContent(DEFAULT_GREETING); - } - - static String getContent(String greeting) { - return "public class " + CLASS_NAME + " {\n" + + @Override + String getContentWithoutPackage(String nwHello) { + return "public class " + getClassName() + " {\n" + " public static void main(String[] args) throws InterruptedException {\n" + " while(true) {\n" + - " System.out.println(\"" + greeting + "\");\n" + - " Thread.sleep(1000);\n" + + " System.out.println(\"" + nwHello + "\");\n" + + " Thread.sleep(100);\n" + " }\n" + " }\n" + "}\n"; } - static String getContentWithPackage(String greeting) { - return "package " + PACKAGE_NAME + ";\n\n" + - getContent(greeting); + @Override + String getGreetings() { + return "Hello"; } - static String getDefaultContentWithPackage() { - return "package " + PACKAGE_NAME + ";\n\n" + - getDefaultContent(); - } - - static String getEmptyClass() { - return "package " + PACKAGE_NAME + ";\n" + - "public class " + CLASS_NAME + " {}"; - } - static class TestingDummyException extends Exception { - TestingDummyException(String message) { - super(message); - } - - TestingDummyException(String message, Throwable cause) { - super(message, cause); - } - } }