From ca8de1356cb921519d1c916cf5e7b25f0a366a8a Mon Sep 17 00:00:00 2001 From: Shanyu Thibaut Juneja Date: Tue, 24 Dec 2024 09:25:24 +0200 Subject: [PATCH] fix: improvements to MapleIR ssa optimisation --- build.gradle | 28 +- dev.skidfuscator.obfuscator/build.gradle | 8 - .../skidfuscator/obfuscator/Skidfuscator.java | 12 +- .../obfuscator/event/EventBus.java | 13 +- .../number/pure/ImagineBreaker.java | 330 ---------------- .../number/pure/VmHashTransformer.java | 2 - .../AbstractExpressionTransformer.java | 64 +++ .../transform/AbstractTransformer.java | 11 + .../obfuscator/transform/Transformer.java | 5 + .../impl/hash/InstanceOfHashTransformer.java | 76 ++++ .../impl/hash/StringEqualsHashTranformer.java | 112 ------ .../hash/StringEqualsHashTransformer.java | 58 +++ .../StringEqualsIgnoreCaseHashTranformer.java | 114 ------ ...StringEqualsIgnoreCaseHashTransformer.java | 65 +++ .../impl/sdk/SdkInjectorTransformer.java | 13 +- .../dev/skidfuscator/test/ConfigTest.java | 4 +- .../dev/skidfuscator/test/SampleJarTest.java | 2 +- .../test/evaluator/EvaluatorTest.java | 6 + .../test/type/InstanceOfTest.java | 19 + .../skidfuscator/test/vm/VmBootstrapTest.java | 3 - .../testclasses/type/InstanceOf.java | 10 + dev.skidfuscator.sdk/build.gradle | 10 + .../src/main/java/sdk/SDK.java | 54 +++ .../org.mapleir.stdlib/build.gradle | 14 +- .../collections/graph/algorithms/LT79Dom.java | 132 +++++++ .../graph/AbstractFastGraphTest.java | 4 +- .../collections/graph/algorithms/DfsTest.java | 22 +- .../graph/algorithms/ReducibleLoopTest.java | 4 + .../graph/directed/FastDirectedGraphTest.java | 19 +- .../graph/dom/LT79DomLoopTest.java | 370 ++++++++++++++++++ .../undirected/FastUndirectedGraphTest.java | 5 +- .../graph/util/CollectionUtil.java | 2 +- .../graph/util/FakeFastVertex.java | 15 +- .../graph/util/GraphTestBuilder.java | 94 +++++ 34 files changed, 1095 insertions(+), 605 deletions(-) delete mode 100644 dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/number/pure/ImagineBreaker.java create mode 100644 dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/AbstractExpressionTransformer.java create mode 100644 dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/InstanceOfHashTransformer.java delete mode 100644 dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsHashTranformer.java create mode 100644 dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsHashTransformer.java delete mode 100644 dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsIgnoreCaseHashTranformer.java create mode 100644 dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsIgnoreCaseHashTransformer.java create mode 100644 dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/type/InstanceOfTest.java create mode 100644 dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/testclasses/type/InstanceOf.java create mode 100644 org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/dom/LT79DomLoopTest.java create mode 100644 org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/util/GraphTestBuilder.java diff --git a/build.gradle b/build.gradle index e20dc099..d11b0db2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,6 @@ +import org.gradle.api.JavaVersion +import org.gradle.api.tasks.compile.JavaCompile + plugins { id 'java' id 'java-library' @@ -16,11 +19,10 @@ allprojects { } } - apply plugin: 'java' apply plugin: 'java-library' - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 dependencies { compileOnly 'org.projectlombok:lombok:1.18.36' @@ -34,6 +36,26 @@ allprojects { options.encoding = "UTF-8" // Will fail on the non-ascii comments if not set } + tasks.withType(JavaCompile).configureEach { + if (sourceCompatibility == JavaVersion.VERSION_17) { + options.compilerArgs += [ + '--add-exports', 'java.base/jdk.internal.reflect=ALL-UNNAMED', + '--add-exports', 'java.base/jdk.internal.util=ALL-UNNAMED', + '--add-exports', 'java.base/sun.io.ch=ALL-UNNAMED' + ] + } + } + tasks.withType(Test).configureEach { + if (sourceCompatibility == JavaVersion.VERSION_17) { + jvmArgs += [ + '--add-exports', 'java.base/jdk.internal.reflect=ALL-UNNAMED', + '--add-exports', 'java.base/jdk.internal.util=ALL-UNNAMED', + '--add-exports', 'java.base/sun.io.ch=ALL-UNNAMED' + ] + } + } + + ext { asm = 'org.ow2.asm:asm:9.7.1' asm_commons = 'org.ow2.asm:asm-commons:9.7.1' diff --git a/dev.skidfuscator.obfuscator/build.gradle b/dev.skidfuscator.obfuscator/build.gradle index 2c57e7b0..5f97a8f8 100644 --- a/dev.skidfuscator.obfuscator/build.gradle +++ b/dev.skidfuscator.obfuscator/build.gradle @@ -39,12 +39,6 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' } -tasks.withType(JavaCompile) { - options.compilerArgs += [ - '--add-exports', 'java.base/jdk.internal.reflect=ALL-UNNAMED', - '--add-exports', 'java.base/jdk.internal.util=ALL-UNNAMED' - ] -} jar { into('resources') { @@ -68,8 +62,6 @@ processTestResources { test { useJUnitPlatform() } -sourceCompatibility = JavaVersion.VERSION_17 -targetCompatibility = JavaVersion.VERSION_17 configurations { downgrade diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/Skidfuscator.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/Skidfuscator.java index 4a970372..70bc42df 100644 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/Skidfuscator.java +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/Skidfuscator.java @@ -53,13 +53,14 @@ import dev.skidfuscator.obfuscator.transform.impl.flow.exception.BasicExceptionTransformer; import dev.skidfuscator.obfuscator.transform.impl.flow.interprocedural.InterproceduralTransformer; import dev.skidfuscator.obfuscator.transform.impl.flow.interprocedural.RandomInitTransformer; -import dev.skidfuscator.obfuscator.transform.impl.hash.StringEqualsIgnoreCaseHashTranformer; +import dev.skidfuscator.obfuscator.transform.impl.hash.InstanceOfHashTransformer; +import dev.skidfuscator.obfuscator.transform.impl.hash.StringEqualsHashTransformer; +import dev.skidfuscator.obfuscator.transform.impl.hash.StringEqualsIgnoreCaseHashTransformer; import dev.skidfuscator.obfuscator.transform.impl.misc.AhegaoTransformer; import dev.skidfuscator.obfuscator.transform.impl.number.NumberTransformer; import dev.skidfuscator.obfuscator.transform.impl.pure.PureHashTransformer; import dev.skidfuscator.obfuscator.transform.impl.sdk.SdkInjectorTransformer; import dev.skidfuscator.obfuscator.transform.impl.string.StringEncryptionType; -import dev.skidfuscator.obfuscator.transform.impl.hash.StringEqualsHashTranformer; import dev.skidfuscator.obfuscator.transform.impl.string.StringTransformerV2; import dev.skidfuscator.obfuscator.util.ConsoleColors; import dev.skidfuscator.obfuscator.util.MapleJarUtil; @@ -705,8 +706,9 @@ public List getTransformers() { new BasicRangeTransformer(this), new PureHashTransformer(this), new SdkInjectorTransformer(this), - new StringEqualsHashTranformer(this), - new StringEqualsIgnoreCaseHashTranformer(this), + new StringEqualsHashTransformer(this), + new StringEqualsIgnoreCaseHashTransformer(this), + new InstanceOfHashTransformer(this), /* new FlatteningFlowTransformer(this),*/ new AhegaoTransformer(this) @@ -723,7 +725,7 @@ public List getTransformers() { transformers.clear(); for (Transformer temp : temps) { - if (temp.getConfig().isEnabled()) { + if (temp.isEnabled()) { transformers.add(temp); //System.out.println(temp.getName() + " -> " + Arrays.toString(temp.getConfig().getExemptions().toArray())); for (String exemption : temp.getConfig().getExemptions()) { diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/event/EventBus.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/event/EventBus.java index acb26ee3..a4501414 100644 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/event/EventBus.java +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/event/EventBus.java @@ -2,6 +2,7 @@ import dev.skidfuscator.obfuscator.event.annotation.Listen; import dev.skidfuscator.obfuscator.event.impl.Event; +import dev.skidfuscator.obfuscator.transform.AbstractTransformer; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -21,7 +22,17 @@ public class EventBus { * @param instance Instance of the listener to be registered */ public static void register(final Listener instance) { - for (Method declaredMethod : instance.getClass().getDeclaredMethods()) { + final Set methods = new HashSet<>(); + + // get parent until abstract transformer is reached + Class clazz = instance.getClass(); + + while (!clazz.equals(Listener.class) && !clazz.equals(Object.class)) { + methods.addAll(Arrays.asList(clazz.getDeclaredMethods())); + clazz = clazz.getSuperclass(); + } + + for (Method declaredMethod : methods) { if (!declaredMethod.isAnnotationPresent(Listen.class)) continue; diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/number/pure/ImagineBreaker.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/number/pure/ImagineBreaker.java deleted file mode 100644 index e67a5a59..00000000 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/number/pure/ImagineBreaker.java +++ /dev/null @@ -1,330 +0,0 @@ -package dev.skidfuscator.obfuscator.number.pure; - -import jdk.internal.reflect.Reflection; -import sun.misc.Unsafe; - -import java.lang.Module; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles.Lookup; -import java.lang.invoke.MethodType; -import java.lang.invoke.VarHandle; -import java.lang.ref.SoftReference; -import java.lang.reflect.Field; -import java.util.AbstractMap; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -/** - * @since 2.0 - * No natives required, no starting arguments required. - * This is achieved thanks to jdk.internal.reflect.Reflection class not banning methods - * from being reflectively invoked. With this, we can gain a lot of ground. - * Unfortunately, it is likely that this will be patched very soon. - * Extensive testing is provided for nearly all major jdk versions and vendors. - */ -public final class ImagineBreaker { - - public static final boolean IS_OPEN_J9 = isOpenJ9(); - - private static final Unsafe UNSAFE = retrieveUnsafe(); - private static final Lookup LOOKUP = retrieveLookup(); - private static final MethodHandle MODULE$ADD_EXPORTS_TO_ALL_0 = retrieveAddExportsToAll0(); - - private static final Set EVERYONE_MODULE_SET = retrieveEveryoneSet(); - private static final VarHandle MODULE$OPEN_PACKAGES = retrieveOpenPackagesHandle(); - private static final VarHandle CLASS$MODULE = retrieveModuleHandle(); - private static final VarHandle REFLECTION$FIELD_FILTER_MAP = retrieveFieldFilterMap(); - private static final VarHandle REFLECTION$METHOD_FILTER_MAP = retrieveMethodFilterMap(); - private static final VarHandle CLASS$REFLECTION_DATA = retrieveReflectionData(); - - /** - * Accessor to sun.misc.Unsafe. - * - * @return instance of sun.misc.Unsafe. - */ - public static Unsafe unsafe() { - return UNSAFE; - } - - /** - * Accessor to the trusted MethodHandles$Lookup. - * - * @return instance of the trusted lookup. - */ - public static Lookup lookup() { - return LOOKUP; - } - - /** - * Opens all modules within the boot ModuleLayer. - */ - public static void openBootModules() { - openModuleLayer(ModuleLayer.boot()); - } - - public static void openBootModule(String bootModule) { - openModule(ModuleLayer.boot().findModule(bootModule).orElseThrow()); - } - - /** - * Opens all modules within the specified ModuleLayer - * - * @param layer module layer to have all of its modules opened - */ - public static void openModuleLayer(ModuleLayer layer) { - layer.modules().forEach(ImagineBreaker::openModule); - } - - /** - * Opens a specific module - * - * @param module module to be opened - */ - public static void openModule(Module module) { - MODULE$OPEN_PACKAGES.set(module, WorldRejector.INSTANCE); - for (String pkg : module.getPackages()) { - try { - MODULE$ADD_EXPORTS_TO_ALL_0.invokeExact(module, pkg); - } catch (Throwable e) { - throw new RuntimeException(e); - } - } - } - - /** - * Disguises a class as having a module of a different class. - * Extremely useful when invoking caller-sensitive methods. - * - *

Consider {@link ImagineBreaker#disguiseAsModule(Class, Class, Runnable)} - * if you want to revert the disguise. Use the runnable to run actions before the reversion

- * - * @param target target class to have its module changed - * @param moduleClass class to have its module queried - */ - public static void disguiseAsModule(Class target, Class moduleClass) { - Object module = CLASS$MODULE.get(moduleClass); - CLASS$MODULE.set(target, module); - } - - /** - * Disguises a class as having a different module. - * Extremely useful when invoking caller-sensitive methods. - * - *

Consider {@link ImagineBreaker#disguiseAsModule(Class, Module, Runnable)} - * if you want to revert the disguise. Use the runnable to run actions before the reversion

- * - * @param target target class to have its module changed - * @param module module for the target class - */ - public static void disguiseAsModule(Class target, Module module) { - CLASS$MODULE.set(target, module); - } - - /** - * Disguises a class as having a different module. - * Extremely useful when invoking caller-sensitive methods. - * This method allows reversion of module for the target class after the runnable is ran. - * - * @param target target class to have its module changed - * @param moduleClass module for the target class - * @param runnable runnable to run before the class is reverted to having its module changed - */ - public static void disguiseAsModule(Class target, Class moduleClass, Runnable runnable) { - Object old = CLASS$MODULE.get(target); - disguiseAsModule(target, moduleClass); - runnable.run(); - CLASS$MODULE.set(target, old); - } - - /** - * Disguises a class as having a different module. - * Extremely useful when invoking caller-sensitive methods. - * This method allows reversion of module for the target class after the runnable is ran. - * - * @param target target class to have its module changed - * @param module module for the target class - * @param runnable runnable to run before the class is reverted to having its module changed - */ - public static void disguiseAsModule(Class target, Module module, Runnable runnable) { - Object old = CLASS$MODULE.get(target); - disguiseAsModule(target, module); - runnable.run(); - CLASS$MODULE.set(target, old); - } - - /** - * Wipes {@link jdk.internal.reflect.Reflection#fieldFilterMap} as well as the - * {@link java.lang.Class#reflectionData} member in their respective classes, though this is not done for OpenJ9. - * This method should be called first to eliminate the default filters - * but since the method to register field filters is copy-on-write - * new filters may be added after-the-fact, meaning this method needs to be called again. - */ - public static void wipeFieldFilters() { - if (!IS_OPEN_J9) { - for (Class clazz : ((Map) REFLECTION$FIELD_FILTER_MAP.get()).keySet()) { - CLASS$REFLECTION_DATA.setVolatile(clazz, null); - } - } - REFLECTION$FIELD_FILTER_MAP.setVolatile(new HashMap<>()); - } - - /** - * Wipes {@link jdk.internal.reflect.Reflection#methodFilterMap} as well as the - * {@link java.lang.Class#reflectionData} member in their respective classes, though this is not done for OpenJ9. - * This method should be called first to eliminate the default filters - * but since the method to register method filters is copy-on-write - * new filters may be added after-the-fact, meaning this method needs to be called again. - */ - public static void wipeMethodFilters() { - if (!IS_OPEN_J9) { - for (Class clazz : ((Map) REFLECTION$METHOD_FILTER_MAP.get()).keySet()) { - CLASS$REFLECTION_DATA.setVolatile(clazz, null); - } - } - REFLECTION$METHOD_FILTER_MAP.setVolatile(new HashMap<>()); - } - - private static boolean isOpenJ9() { - return "Eclipse OpenJ9".equals(System.getProperty("java.vm.vendor")); - } - - private static Unsafe retrieveUnsafe() { - try { - Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); - theUnsafe.setAccessible(true); - return (Unsafe) theUnsafe.get(null); - } catch (IllegalAccessException | NoSuchFieldException e) { - throw new RuntimeException(e); - } - } - - private static Lookup retrieveLookup() { - Field methodHandles$lookup$implLookup = retrieveImplLookup(); - long offset = UNSAFE.staticFieldOffset(methodHandles$lookup$implLookup); - return (Lookup) UNSAFE.getObject(Lookup.class, offset); - } - - private static Field retrieveImplLookup() { - try { - return Lookup.class.getDeclaredField("IMPL_LOOKUP"); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } - - private static MethodHandle retrieveAddExportsToAll0() { - try { - return LOOKUP.findStatic(Module.class, "addExportsToAll0", MethodType.methodType(void.class, Module.class, String.class)); - } catch (NoSuchMethodException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - private static Set retrieveEveryoneSet() { - try { - return (Set) LOOKUP.findStaticVarHandle(Module.class, "EVERYONE_SET", Set.class).get(); - } catch (IllegalAccessException e1) { - if (e1.getMessage().endsWith("Expected static field.")) { - try { - return (Set) mockLookup(Module.class).findStaticVarHandle(Module.class, "EVERYONE_SET", Set.class).get(); - } catch (Throwable e) { - throw new RuntimeException(e); - } - } - throw new RuntimeException(e1); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } - - private static VarHandle retrieveOpenPackagesHandle() { - try { - return LOOKUP.findVarHandle(Module.class, "openPackages", Map.class); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - private static VarHandle retrieveModuleHandle() { - try { - return LOOKUP.findVarHandle(Class.class, "module", Module.class); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - private static VarHandle retrieveFieldFilterMap() { - try { - openBootModule("java.base"); - return LOOKUP.findStaticVarHandle(Reflection.class, "fieldFilterMap", Map.class); - } catch (IllegalAccessException e1) { - if (e1.getMessage().endsWith("Expected static field.")) { - try { - return mockLookup(Reflection.class).findStaticVarHandle(Reflection.class, "fieldFilterMap", Map.class); - } catch (Throwable e) { - throw new RuntimeException(e); - } - } - throw new RuntimeException(e1); - } catch (NoSuchFieldException e2) { - throw new RuntimeException(e2); - } - } - - private static VarHandle retrieveMethodFilterMap() { - try { - openBootModule("java.base"); - return LOOKUP.findStaticVarHandle(Reflection.class, "methodFilterMap", Map.class); - } catch (IllegalAccessException e1) { - if (e1.getMessage().endsWith("Expected static field.")) { - try { - return mockLookup(Reflection.class).findStaticVarHandle(Reflection.class, "methodFilterMap", Map.class); - } catch (Throwable e) { - throw new RuntimeException(e); - } - } - throw new RuntimeException(e1); - } catch (NoSuchFieldException e2) { - throw new RuntimeException(e2); - } - } - - private static VarHandle retrieveReflectionData() { - try { - return LOOKUP.findVarHandle(Class.class, "reflectionData", SoftReference.class); - } catch (NoSuchFieldException | IllegalAccessException e) { - if (!IS_OPEN_J9) { - throw new RuntimeException(e); - } - return null; - } - } - - // Extremely crude hack, used for OpenJ9 9 - 15, where nothing ever made sense - private static Lookup mockLookup(Class mockClass) throws Throwable { - MethodHandle lookup$ctor = LOOKUP.findConstructor(Lookup.class, - MethodType.methodType(void.class, Class.class, Class.class, int.class, boolean.class)); - return (Lookup) lookup$ctor.invokeExact(mockClass, (Class) null, 31, false); // Magic number - } - - private ImagineBreaker() { } - - private static class WorldRejector extends AbstractMap> { - - private static final WorldRejector INSTANCE = new WorldRejector(); - - @Override - public Set get(Object key) { - return EVERYONE_MODULE_SET; - } - - @Override - public Set>> entrySet() { - return Collections.emptySet(); - } - - } - -} \ No newline at end of file diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/number/pure/VmHashTransformer.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/number/pure/VmHashTransformer.java index 5bdddd87..7751d140 100644 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/number/pure/VmHashTransformer.java +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/number/pure/VmHashTransformer.java @@ -237,8 +237,6 @@ public Expr hash(BasicBlock vertex, PredicateFlowGetter caller) { @SneakyThrows private void init() { - ImagineBreaker.openBootModules(); - // -- SENSITIVE FUCKERY DO NOT TOUCH this.vm = new VirtualMachine() { diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/AbstractExpressionTransformer.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/AbstractExpressionTransformer.java new file mode 100644 index 00000000..44c67d32 --- /dev/null +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/AbstractExpressionTransformer.java @@ -0,0 +1,64 @@ +package dev.skidfuscator.obfuscator.transform; + +import dev.skidfuscator.obfuscator.Skidfuscator; +import dev.skidfuscator.obfuscator.event.EventPriority; +import dev.skidfuscator.obfuscator.event.annotation.Listen; +import dev.skidfuscator.obfuscator.event.impl.transform.method.RunMethodTransformEvent; +import dev.skidfuscator.obfuscator.skidasm.SkidMethodNode; +import dev.skidfuscator.obfuscator.skidasm.cfg.SkidBlock; +import org.mapleir.ir.cfg.BasicBlock; +import org.mapleir.ir.cfg.ControlFlowGraph; +import org.mapleir.ir.code.Expr; +import org.mapleir.ir.code.Stmt; + +import java.util.HashSet; + +public abstract class AbstractExpressionTransformer extends AbstractTransformer { + public AbstractExpressionTransformer(Skidfuscator skidfuscator, String name) { + super(skidfuscator, name); + } + + @Listen(EventPriority.LOWEST) + void handle(final RunMethodTransformEvent event) { + final SkidMethodNode methodNode = event.getMethodNode(); + + if (shouldSkipMethod(methodNode)) { + this.skip(); + return; + } + + final ControlFlowGraph cfg = methodNode.getCfg(); + if (cfg == null) { + this.fail(); + return; + } + + for (BasicBlock vertex : new HashSet<>(cfg.vertices())) { + if (vertex.isFlagSet(SkidBlock.FLAG_NO_OPAQUE)) + continue; + + if (methodNode.isClinit()) { + continue; + } + + for (Stmt stmt : new HashSet<>(vertex)) { + for (Expr expr : stmt.enumerateOnlyChildren()) { + if (matchesExpression(expr)) { + if (transformExpression(expr, cfg)) { + this.success(); + } + } + } + } + } + } + + protected boolean shouldSkipMethod(SkidMethodNode methodNode) { + return methodNode.isAbstract() + || methodNode.isInit() + || methodNode.node.instructions.size() > 10000; + } + + protected abstract boolean matchesExpression(Expr expr); + protected abstract boolean transformExpression(Expr expr, ControlFlowGraph cfg); +} \ No newline at end of file diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/AbstractTransformer.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/AbstractTransformer.java index 1f41a08c..357f1b5d 100644 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/AbstractTransformer.java +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/AbstractTransformer.java @@ -21,6 +21,7 @@ public abstract class AbstractTransformer implements Transformer { private int success; private int skipped; private int failed; + private boolean requiresSdk; public AbstractTransformer(Skidfuscator skidfuscator, String name) { this(skidfuscator, name, Collections.emptyList()); @@ -37,6 +38,12 @@ protected T createConfig() { return (T) new DefaultTransformerConfig(skidfuscator.getTsConfig(), MiscUtil.toCamelCase(name)); } + public boolean isEnabled() { + return config.isEnabled() + && (!requiresSdk + || skidfuscator.getConfig().getBoolean("sdk.enabled", true)); + } + public DefaultTransformerConfig getConfig() { return config; } @@ -49,6 +56,10 @@ public void register() { } } + public void requiresSdk() { + this.requiresSdk = true; + } + @Override public String getName() { return name; diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/Transformer.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/Transformer.java index 214775ca..4f86a132 100644 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/Transformer.java +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/Transformer.java @@ -31,4 +31,9 @@ public interface Transformer extends Listener { * @return Formatted ANSI string of the result of thr transformer */ String getResult(); + + /** + * @return If the transformer is enabled + */ + boolean isEnabled(); } diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/InstanceOfHashTransformer.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/InstanceOfHashTransformer.java new file mode 100644 index 00000000..78f455f0 --- /dev/null +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/InstanceOfHashTransformer.java @@ -0,0 +1,76 @@ +package dev.skidfuscator.obfuscator.transform.impl.hash; + +import dev.skidfuscator.obfuscator.Skidfuscator; +import dev.skidfuscator.obfuscator.number.NumberManager; +import dev.skidfuscator.obfuscator.predicate.factory.PredicateFlowGetter; +import dev.skidfuscator.obfuscator.predicate.opaque.BlockOpaquePredicate; +import dev.skidfuscator.obfuscator.skidasm.SkidMethodNode; +import dev.skidfuscator.obfuscator.skidasm.cfg.SkidBlock; +import dev.skidfuscator.obfuscator.transform.AbstractExpressionTransformer; +import dev.skidfuscator.obfuscator.transform.impl.number.NumberTransformer; +import dev.skidfuscator.obfuscator.util.RandomUtil; +import org.mapleir.ir.cfg.ControlFlowGraph; +import org.mapleir.ir.code.Expr; +import org.mapleir.ir.code.expr.ConstantExpr; +import org.mapleir.ir.code.expr.InstanceofExpr; +import org.mapleir.ir.code.expr.invoke.StaticInvocationExpr; +import org.objectweb.asm.Type; +import sdk.LongHashFunction; + +public class InstanceOfHashTransformer extends AbstractExpressionTransformer { + public InstanceOfHashTransformer(Skidfuscator skidfuscator) { + super(skidfuscator, "Type Check"); + requiresSdk(); + } + + private final int seed = RandomUtil.nextInt(); + + @Override + protected boolean matchesExpression(Expr expr) { + final boolean valid = expr instanceof InstanceofExpr; + + if (valid) { + System.out.println("Checking instanceof expression: " + expr); + } + + return valid; + } + + @Override + protected boolean transformExpression(Expr expr, ControlFlowGraph cfg) { + final SkidMethodNode methodNode = (SkidMethodNode) cfg.getMethodNode(); + final int blockPredicate = methodNode.getBlockPredicate((SkidBlock) expr.getBlock()); + final BlockOpaquePredicate predicate = methodNode.getFlowPredicate(); + + InstanceofExpr instanceofExpr = (InstanceofExpr) expr; + Type checkType = instanceofExpr.getCheckType(); + Expr object = instanceofExpr.getExpression(); + + System.out.println("Checking instanceof expression: " + expr); + + // Skip primitive types and arrays + if (checkType.getSort() != Type.OBJECT || checkType.getDescriptor().startsWith("[")) { + return false; + } + + // Create a new static invocation to our custom type check method + StaticInvocationExpr replacement = new StaticInvocationExpr( + new Expr[] { + object.copy(), + // Pass the type hash as a string constant + new ConstantExpr("" + LongHashFunction + .xx3(seed) + .hashChars(checkType.getInternalName()) + ), + NumberManager.encrypt(seed, blockPredicate, expr.getBlock(), predicate.getGetter()) + }, + "sdk/SDK", + "checkType", + "(Ljava/lang/Object;Ljava/lang/String;I)Z" + ); + + System.out.println("Transformed instanceof expression: " + expr + " to " + replacement); + expr.getParent().overwrite(expr, replacement); + return true; + } +} \ No newline at end of file diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsHashTranformer.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsHashTranformer.java deleted file mode 100644 index 729d45c2..00000000 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsHashTranformer.java +++ /dev/null @@ -1,112 +0,0 @@ -package dev.skidfuscator.obfuscator.transform.impl.hash; - -import dev.skidfuscator.obfuscator.Skidfuscator; -import dev.skidfuscator.obfuscator.event.EventPriority; -import dev.skidfuscator.obfuscator.event.annotation.Listen; -import dev.skidfuscator.obfuscator.event.impl.transform.method.RunMethodTransformEvent; -import dev.skidfuscator.obfuscator.skidasm.SkidMethodNode; -import dev.skidfuscator.obfuscator.skidasm.cfg.SkidBlock; -import dev.skidfuscator.obfuscator.transform.AbstractTransformer; -import dev.skidfuscator.obfuscator.util.cfg.Variables; -import org.mapleir.ir.cfg.BasicBlock; -import org.mapleir.ir.cfg.ControlFlowGraph; -import org.mapleir.ir.code.Expr; -import org.mapleir.ir.code.Stmt; -import org.mapleir.ir.code.expr.ConstantExpr; -import org.mapleir.ir.code.expr.VarExpr; -import org.mapleir.ir.code.expr.invoke.InvocationExpr; -import org.mapleir.ir.code.expr.invoke.StaticInvocationExpr; -import sdk.LongHashFunction; - -import java.util.HashSet; - -public class StringEqualsHashTranformer extends AbstractTransformer { - public StringEqualsHashTranformer(final Skidfuscator skidfuscator) { - super(skidfuscator, "String Equals Hash"); - } - - @Listen(EventPriority.LOWEST) - void handle(final RunMethodTransformEvent event) { - final SkidMethodNode methodNode = event.getMethodNode(); - - if (methodNode.isAbstract() - || methodNode.isInit()) { - this.skip(); - return; - } - - if (methodNode.node.instructions.size() > 10000) { - this.fail(); - return; - } - - final ControlFlowGraph cfg = methodNode.getCfg(); - - if (cfg == null) { - this.fail(); - return; - } - - for (BasicBlock vertex : new HashSet<>(cfg.vertices())) { - if (vertex.isFlagSet(SkidBlock.FLAG_NO_OPAQUE)) - continue; - - if (methodNode.isClinit()) { - continue; - } - - for (Stmt stmt : new HashSet<>(vertex)) { - for (Expr expr : stmt.enumerateOnlyChildren()) { - if (expr instanceof InvocationExpr invocationExpr) { - final boolean isEqualsMethod = - invocationExpr.getOwner().equals("java/lang/String") - && invocationExpr.getName().equals("equals") - && invocationExpr.getDesc().equals("(Ljava/lang/Object;)Z"); - - if (methodNode.owner.getName().contains("BlowfishTest")) { - System.out.println(expr); - } - - if (!isEqualsMethod) - continue; - - System.out.println(String.format("Found %s with matching%n", expr)); - - final Expr[] args = invocationExpr.getArgumentExprs(); - Expr arg0 = args[0]; - Expr arg1 = args[1]; - - if (arg0 instanceof VarExpr var0) { - arg0 = Variables.getDefinition(cfg, var0); - } - - if (arg1 instanceof VarExpr var1) { - arg1 = Variables.getDefinition(cfg, var1); - } - - final boolean isArg0Constant = arg0 instanceof ConstantExpr; - final boolean isArg1Constant = arg1 instanceof ConstantExpr; - - if (isArg0Constant == isArg1Constant) { - continue; - } - - ConstantExpr constantExpr = isArg0Constant ? (ConstantExpr) arg0 : (ConstantExpr) arg1; - Expr otherExpr = isArg0Constant ? arg1 : arg0; - - constantExpr.setConstant( - "" + LongHashFunction.xx3().hashChars((String) constantExpr.getConstant()) - ); - otherExpr.getParent().overwrite(otherExpr, new StaticInvocationExpr( - new Expr[] { otherExpr.copy() }, - "sdk/SDK", - "hash", - "(Ljava/lang/String;)Ljava/lang/String;" - )); - this.success(); - } - } - } - } - } -} diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsHashTransformer.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsHashTransformer.java new file mode 100644 index 00000000..ae4c3e7a --- /dev/null +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsHashTransformer.java @@ -0,0 +1,58 @@ +package dev.skidfuscator.obfuscator.transform.impl.hash; + +import dev.skidfuscator.obfuscator.Skidfuscator; +import dev.skidfuscator.obfuscator.transform.AbstractExpressionTransformer; +import org.mapleir.ir.cfg.ControlFlowGraph; +import org.mapleir.ir.code.Expr; +import org.mapleir.ir.code.expr.ConstantExpr; +import org.mapleir.ir.code.expr.invoke.InvocationExpr; +import org.mapleir.ir.code.expr.invoke.StaticInvocationExpr; +import sdk.LongHashFunction; + +public class StringEqualsHashTransformer extends AbstractExpressionTransformer { + public StringEqualsHashTransformer(Skidfuscator skidfuscator) { + super(skidfuscator, "String Equals Hash"); + requiresSdk(); + } + + @Override + protected boolean matchesExpression(Expr expr) { + if (!(expr instanceof InvocationExpr invocationExpr)) { + return false; + } + + return invocationExpr.getOwner().equals("java/lang/String") + && invocationExpr.getName().equals("equals") + && invocationExpr.getDesc().equals("(Ljava/lang/Object;)Z"); + } + + @Override + protected boolean transformExpression(Expr expr, ControlFlowGraph cfg) { + InvocationExpr invocationExpr = (InvocationExpr) expr; + final Expr[] args = invocationExpr.getArgumentExprs(); + Expr arg0 = args[0]; + Expr arg1 = args[1]; + + final boolean isArg0Constant = arg0 instanceof ConstantExpr; + final boolean isArg1Constant = arg1 instanceof ConstantExpr; + + if (isArg0Constant == isArg1Constant) { + return false; + } + + ConstantExpr constantExpr = isArg0Constant ? (ConstantExpr) arg0 : (ConstantExpr) arg1; + Expr otherExpr = isArg0Constant ? arg1 : arg0; + + constantExpr.setConstant( + "" + LongHashFunction.xx3().hashChars((String) constantExpr.getConstant()) + ); + otherExpr.getParent().overwrite(otherExpr, new StaticInvocationExpr( + new Expr[] { otherExpr.copy() }, + "sdk/SDK", + "hash", + "(Ljava/lang/String;)Ljava/lang/String;" + )); + + return true; + } +} \ No newline at end of file diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsIgnoreCaseHashTranformer.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsIgnoreCaseHashTranformer.java deleted file mode 100644 index 7a2fca0e..00000000 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsIgnoreCaseHashTranformer.java +++ /dev/null @@ -1,114 +0,0 @@ -package dev.skidfuscator.obfuscator.transform.impl.hash; - -import dev.skidfuscator.obfuscator.Skidfuscator; -import dev.skidfuscator.obfuscator.event.annotation.Listen; -import dev.skidfuscator.obfuscator.event.impl.transform.method.RunMethodTransformEvent; -import dev.skidfuscator.obfuscator.skidasm.SkidMethodNode; -import dev.skidfuscator.obfuscator.skidasm.cfg.SkidBlock; -import dev.skidfuscator.obfuscator.transform.AbstractTransformer; -import dev.skidfuscator.obfuscator.util.cfg.Variables; -import org.mapleir.ir.cfg.BasicBlock; -import org.mapleir.ir.cfg.ControlFlowGraph; -import org.mapleir.ir.code.Expr; -import org.mapleir.ir.code.Stmt; -import org.mapleir.ir.code.expr.ConstantExpr; -import org.mapleir.ir.code.expr.VarExpr; -import org.mapleir.ir.code.expr.invoke.InvocationExpr; -import org.mapleir.ir.code.expr.invoke.StaticInvocationExpr; -import org.mapleir.ir.code.expr.invoke.VirtualInvocationExpr; -import sdk.LongHashFunction; - -import java.util.HashSet; - -public class StringEqualsIgnoreCaseHashTranformer extends AbstractTransformer { - public StringEqualsIgnoreCaseHashTranformer(final Skidfuscator skidfuscator) { - super(skidfuscator, "String EqIgCase Hash"); - } - - @Listen - void handle(final RunMethodTransformEvent event) { - final SkidMethodNode methodNode = event.getMethodNode(); - - if (methodNode.isAbstract() - || methodNode.isInit()) { - this.skip(); - return; - } - - if (methodNode.node.instructions.size() > 10000) { - this.fail(); - return; - } - - final ControlFlowGraph cfg = methodNode.getCfg(); - - if (cfg == null) { - this.fail(); - return; - } - - for (BasicBlock vertex : new HashSet<>(cfg.vertices())) { - if (vertex.isFlagSet(SkidBlock.FLAG_NO_OPAQUE)) - continue; - - if (methodNode.isClinit() && this.heuristicSizeSkip(methodNode, 8.f)) { - continue; - } - - for (Stmt stmt : new HashSet<>(vertex)) { - for (Expr expr : stmt.enumerateOnlyChildren()) { - if (expr instanceof InvocationExpr invocationExpr) { - final boolean isEqualsMethod = - invocationExpr.getOwner().equals("java/lang/String") - && invocationExpr.getName().equals("equalsIgnoreCase") - && invocationExpr.getDesc().equals("(Ljava/lang/String;)Z"); - - if (!isEqualsMethod) - continue; - - System.out.println(String.format("Found %s with matching", expr)); - - final Expr[] args = invocationExpr.getArgumentExprs(); - Expr arg0 = args[0]; - Expr arg1 = args[1]; - - if (arg0 instanceof VarExpr var0) { - arg0 = Variables.getDefinition(cfg, var0); - } - - if (arg1 instanceof VarExpr var1) { - arg1 = Variables.getDefinition(cfg, var1); - } - - final boolean isArg0Constant = arg0 instanceof ConstantExpr; - final boolean isArg1Constant = arg1 instanceof ConstantExpr; - - if (isArg0Constant == isArg1Constant) { - continue; - } - - ConstantExpr constantExpr = isArg0Constant ? (ConstantExpr) arg0 : (ConstantExpr) arg1; - Expr otherExpr = isArg0Constant ? arg1 : arg0; - - constantExpr.setConstant( - "" + LongHashFunction.xx3().hashChars(((String) constantExpr.getConstant()).toLowerCase()) - ); - otherExpr.getParent().overwrite(otherExpr, new StaticInvocationExpr( - new Expr[] { new VirtualInvocationExpr( - InvocationExpr.CallType.VIRTUAL, - new Expr[]{otherExpr.copy()}, - "java/lang/String", - "toLowerCase", - "()Ljava/lang/String;" - )}, - "sdk/SDK", - "hash", - "(Ljava/lang/String;)Ljava/lang/String;" - )); - this.success(); - } - } - } - } - } -} diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsIgnoreCaseHashTransformer.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsIgnoreCaseHashTransformer.java new file mode 100644 index 00000000..bb2b8fb5 --- /dev/null +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/hash/StringEqualsIgnoreCaseHashTransformer.java @@ -0,0 +1,65 @@ +package dev.skidfuscator.obfuscator.transform.impl.hash; + +import dev.skidfuscator.obfuscator.Skidfuscator; +import dev.skidfuscator.obfuscator.transform.AbstractExpressionTransformer; +import org.mapleir.ir.cfg.ControlFlowGraph; +import org.mapleir.ir.code.Expr; +import org.mapleir.ir.code.expr.ConstantExpr; +import org.mapleir.ir.code.expr.invoke.InvocationExpr; +import org.mapleir.ir.code.expr.invoke.StaticInvocationExpr; +import org.mapleir.ir.code.expr.invoke.VirtualInvocationExpr; +import sdk.LongHashFunction; + +public class StringEqualsIgnoreCaseHashTransformer extends AbstractExpressionTransformer { + public StringEqualsIgnoreCaseHashTransformer(final Skidfuscator skidfuscator) { + super(skidfuscator, "String EqIgCase Hash"); + requiresSdk(); + } + + @Override + protected boolean matchesExpression(Expr expr) { + if (!(expr instanceof InvocationExpr invocationExpr)) { + return false; + } + + return invocationExpr.getOwner().equals("java/lang/String") + && invocationExpr.getName().equals("equalsIgnoreCase") + && invocationExpr.getDesc().equals("(Ljava/lang/String;)Z"); + } + + @Override + protected boolean transformExpression(Expr expr, ControlFlowGraph cfg) { + InvocationExpr invocationExpr = (InvocationExpr) expr; + final Expr[] args = invocationExpr.getArgumentExprs(); + Expr arg0 = args[0]; + Expr arg1 = args[1]; + + final boolean isArg0Constant = arg0 instanceof ConstantExpr; + final boolean isArg1Constant = arg1 instanceof ConstantExpr; + + if (isArg0Constant == isArg1Constant) { + return false; + } + + ConstantExpr constantExpr = isArg0Constant ? (ConstantExpr) arg0 : (ConstantExpr) arg1; + Expr otherExpr = isArg0Constant ? arg1 : arg0; + + constantExpr.setConstant( + "" + LongHashFunction.xx3().hashChars(((String) constantExpr.getConstant()).toLowerCase()) + ); + otherExpr.getParent().overwrite(otherExpr, new StaticInvocationExpr( + new Expr[] { new VirtualInvocationExpr( + InvocationExpr.CallType.VIRTUAL, + new Expr[]{otherExpr.copy()}, + "java/lang/String", + "toLowerCase", + "()Ljava/lang/String;" + )}, + "sdk/SDK", + "hash", + "(Ljava/lang/String;)Ljava/lang/String;" + )); + + return true; + } +} \ No newline at end of file diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/sdk/SdkInjectorTransformer.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/sdk/SdkInjectorTransformer.java index 0043b53a..255c77ec 100644 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/sdk/SdkInjectorTransformer.java +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/sdk/SdkInjectorTransformer.java @@ -1,10 +1,14 @@ package dev.skidfuscator.obfuscator.transform.impl.sdk; import dev.skidfuscator.obfuscator.Skidfuscator; +import dev.skidfuscator.obfuscator.creator.SkidApplicationClassSource; import dev.skidfuscator.obfuscator.event.annotation.Listen; import dev.skidfuscator.obfuscator.event.impl.transform.skid.InitSkidTransformEvent; +import dev.skidfuscator.obfuscator.phantom.jphantom.PhantomJarDownloader; import dev.skidfuscator.obfuscator.transform.AbstractTransformer; import dev.skidfuscator.obfuscator.util.MapleJarUtil; +import org.mapleir.app.service.ApplicationClassSource; +import org.mapleir.app.service.LibraryClassSource; import org.mapleir.asm.ClassNode; import org.topdank.byteengineer.commons.data.JarClassData; import org.topdank.byteio.in.SingleJarDownloader; @@ -42,7 +46,7 @@ void handle(final InitSkidTransformEvent event) { } // Import the SDK jar classes - final SingleJarDownloader downloader = MapleJarUtil.importJar( + final PhantomJarDownloader downloader = MapleJarUtil.importPhantomJar( sdkFile, skidfuscator ); @@ -51,6 +55,13 @@ void handle(final InitSkidTransformEvent event) { for (JarClassData classData : downloader.getJarContents().getClassContents()) { skidfuscator.getJarContents().getClassContents().add(classData); } + ApplicationClassSource library = new SkidApplicationClassSource("Library", + false, + downloader.getJarContents(), + skidfuscator + ); + + skidfuscator.getClassSource().addLibraries(new LibraryClassSource(library, 5)); } catch (IOException e) { throw new RuntimeException("Failed to inject SDK", e); diff --git a/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/ConfigTest.java b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/ConfigTest.java index 2a583559..558dd1b7 100644 --- a/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/ConfigTest.java +++ b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/ConfigTest.java @@ -54,7 +54,7 @@ public void testDefaultConfig() { ); assertEquals( true, - driverTransformer.getConfig().isEnabled() + driverTransformer.isEnabled() ); // General enabled check @@ -64,7 +64,7 @@ public void testDefaultConfig() { ); assertEquals( true, - switchTransformer.getConfig().isEnabled() + switchTransformer.isEnabled() ); } diff --git a/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/SampleJarTest.java b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/SampleJarTest.java index 54cf564a..d17119fa 100644 --- a/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/SampleJarTest.java +++ b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/SampleJarTest.java @@ -23,7 +23,7 @@ public class SampleJarTest { - @RepeatedTest(1) + @RepeatedTest(20) public void test2() throws Exception { final File input = new File("src/test/resources/test.jar"); final File output = new File("src/test/resources/test-out.jar"); diff --git a/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/evaluator/EvaluatorTest.java b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/evaluator/EvaluatorTest.java index 920afb4b..6f36a246 100644 --- a/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/evaluator/EvaluatorTest.java +++ b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/evaluator/EvaluatorTest.java @@ -19,6 +19,7 @@ import dev.skidfuscator.testclasses.evaluator.util.crypto.AES; import dev.skidfuscator.testclasses.evaluator.util.crypto.Blowfish; import dev.skidfuscator.testclasses.evaluator.util.stats.Calculations; +import org.junit.jupiter.api.RepeatedTest; public class EvaluatorTest extends SkidTest { @Override @@ -26,6 +27,11 @@ public Class getMainClass() { return EvaluatorMain.class; } + @Override + public void test() { + super.test(); + } + @Override public Class[] getClasses() { return new Class[] { diff --git a/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/type/InstanceOfTest.java b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/type/InstanceOfTest.java new file mode 100644 index 00000000..ef3ba0b4 --- /dev/null +++ b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/type/InstanceOfTest.java @@ -0,0 +1,19 @@ +package dev.skidfuscator.test.type; + +import dev.skidfuscator.core.SkidTest; +import dev.skidfuscator.testclasses.TestRun; +import dev.skidfuscator.testclasses.type.InstanceOf; + +public class InstanceOfTest extends SkidTest { + @Override + public Class getMainClass() { + return InstanceOf.class; + } + + @Override + public Class[] getClasses() { + return new Class[]{ + InstanceOf.class + }; + } +} diff --git a/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/vm/VmBootstrapTest.java b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/vm/VmBootstrapTest.java index 1a28210c..e9413b4e 100644 --- a/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/vm/VmBootstrapTest.java +++ b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/vm/VmBootstrapTest.java @@ -1,6 +1,5 @@ package dev.skidfuscator.test.vm; -import dev.skidfuscator.obfuscator.number.pure.ImagineBreaker; import dev.skidfuscator.obfuscator.ssvm.JdkBootClassFinder; import dev.skidfuscator.obfuscator.ssvm.JdkClassDefiner; import dev.skidfuscator.obfuscator.util.JdkDownloader; @@ -19,8 +18,6 @@ public class VmBootstrapTest { @Test public void testBoot() throws IOException { - ImagineBreaker.openBootModules(); - VirtualMachine vm = new VirtualMachine() { @Override protected BootClassFinder createBootClassFinder() { diff --git a/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/testclasses/type/InstanceOf.java b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/testclasses/type/InstanceOf.java new file mode 100644 index 00000000..75cda346 --- /dev/null +++ b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/testclasses/type/InstanceOf.java @@ -0,0 +1,10 @@ +package dev.skidfuscator.testclasses.type; + +import dev.skidfuscator.testclasses.TestRun; + +public class InstanceOf implements TestRun { + @Override + public void run() { + assert this instanceof Object : "Failed instance of test"; + } +} diff --git a/dev.skidfuscator.sdk/build.gradle b/dev.skidfuscator.sdk/build.gradle index 492aaef2..b650589f 100644 --- a/dev.skidfuscator.sdk/build.gradle +++ b/dev.skidfuscator.sdk/build.gradle @@ -1,3 +1,5 @@ +import org.gradle.api.tasks.compile.JavaCompile + plugins { id 'java-library' } @@ -9,6 +11,14 @@ repositories { mavenCentral() } +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +tasks.withType(JavaCompile) { + options.compilerArgs += [ + ] +} + dependencies { testImplementation platform('org.junit:junit-bom:5.10.0') testImplementation 'org.junit.jupiter:junit-jupiter' diff --git a/dev.skidfuscator.sdk/src/main/java/sdk/SDK.java b/dev.skidfuscator.sdk/src/main/java/sdk/SDK.java index 2c7289cb..4d465e49 100644 --- a/dev.skidfuscator.sdk/src/main/java/sdk/SDK.java +++ b/dev.skidfuscator.sdk/src/main/java/sdk/SDK.java @@ -1,7 +1,61 @@ package sdk; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + public class SDK { public static String hash(String s) { return LongHashFunction.xx3().hashChars(s) + ""; } + + private static final Map, String> TYPE_HASHES = new ConcurrentHashMap<>(); + private static final Map, Set> HIERARCHY_HASHES = new ConcurrentHashMap<>(); + + public static boolean checkType(Object obj, String typeHash, int seed) { + if (obj == null) { + return false; + } + + Class objClass = obj.getClass(); + + // Get or compute the full hierarchy hash set for this class + Set hierarchyHashes = HIERARCHY_HASHES.computeIfAbsent( + objClass, + e -> computeHierarchyHashes(objClass, seed) + ); + + return hierarchyHashes.contains(typeHash); + } + + private static Set computeHierarchyHashes(Class cls, int seed) { + Set hashes = new HashSet<>(); + Queue> toProcess = new LinkedList<>(); + Set> processed = new HashSet<>(); + + toProcess.add(cls); + + while (!toProcess.isEmpty()) { + Class current = toProcess.poll(); + + if (current == null || !processed.add(current)) { + continue; + } + + // Add hash for current class + hashes.add(TYPE_HASHES.computeIfAbsent(current, + c -> "" + LongHashFunction.xx3(seed) + .hashChars(c.getName().replace('.', '/')))); + + // Add superclass to process + Class superClass = current.getSuperclass(); + if (superClass != null) { + toProcess.add(superClass); + } + + // Add all interfaces to process (including inherited interfaces) + toProcess.addAll(Arrays.asList(current.getInterfaces())); + } + + return Collections.unmodifiableSet(hashes); + } } diff --git a/org.mapleir.parent/org.mapleir.stdlib/build.gradle b/org.mapleir.parent/org.mapleir.stdlib/build.gradle index cbb215f9..61c177e6 100644 --- a/org.mapleir.parent/org.mapleir.stdlib/build.gradle +++ b/org.mapleir.parent/org.mapleir.stdlib/build.gradle @@ -5,9 +5,21 @@ plugins { dependencies { api project(':dot4j') api project(':property-framework') - testImplementation 'junit:junit:3.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' } +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + exceptionFormat "full" + } +} + + + group = 'dev.skidfuscator.mapleir' description = 'MapleIR-stdlib' diff --git a/org.mapleir.parent/org.mapleir.stdlib/src/main/java/org/mapleir/stdlib/collections/graph/algorithms/LT79Dom.java b/org.mapleir.parent/org.mapleir.stdlib/src/main/java/org/mapleir/stdlib/collections/graph/algorithms/LT79Dom.java index f084d328..f80b6acb 100644 --- a/org.mapleir.parent/org.mapleir.stdlib/src/main/java/org/mapleir/stdlib/collections/graph/algorithms/LT79Dom.java +++ b/org.mapleir.parent/org.mapleir.stdlib/src/main/java/org/mapleir/stdlib/collections/graph/algorithms/LT79Dom.java @@ -1,11 +1,13 @@ package org.mapleir.stdlib.collections.graph.algorithms; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Stack; import org.mapleir.stdlib.collections.graph.FastDirectedGraph; import org.mapleir.stdlib.collections.graph.FastGraphEdge; @@ -315,4 +317,134 @@ public Set getIteratedDominanceFrontier(N v) { throw new UnsupportedOperationException(); } } + + public Map> findNaturalLoops() { + Map> naturalLoops = new HashMap<>(); + + // First identify back edges by doing a DFS traversal + Set visited = new HashSet<>(); + Set inProgress = new HashSet<>(); + dfsIdentifyBackEdges(root, visited, inProgress, naturalLoops); + + return naturalLoops; + } + + private void dfsIdentifyBackEdges(N current, Set visited, Set inProgress, Map> naturalLoops) { + visited.add(current); + inProgress.add(current); + + for (E edge : graph.getEdges(current)) { + N succ = edge.dst(); + + if (inProgress.contains(succ)) { + // Found a back edge - this creates a loop + Set loopBody = identifyLoopBody(succ, current); + naturalLoops.merge(succ, loopBody, (existing, newSet) -> { + existing.addAll(newSet); + return existing; + }); + } else if (!visited.contains(succ)) { + dfsIdentifyBackEdges(succ, visited, inProgress, naturalLoops); + } + } + + inProgress.remove(current); + } + + /** + * Identifies the body of a natural loop given its header and back edge source + */ + private Set identifyLoopBody(N header, N backEdgeSource) { + // Special case for self-loops + if (header == backEdgeSource) { + return new HashSet<>(Collections.singleton(header)); + } + + Set loopBody = new HashSet<>(); + Stack workList = new Stack<>(); + + // Add back edge source to start + workList.push(backEdgeSource); + loopBody.add(backEdgeSource); + loopBody.add(header); // Always include the header + + // Find all nodes that can reach the back edge source without going through the header + while (!workList.isEmpty()) { + N current = workList.pop(); + + // Look at all predecessors + for (E predEdge : graph.getReverseEdges(current)) { + N pred = predEdge.src(); + // Include nodes we haven't seen yet, but don't process header's predecessors + if (!loopBody.contains(pred)) { + loopBody.add(pred); + if (pred != header) { // Don't process header's predecessors + workList.push(pred); + } + } + } + } + + return loopBody; + } + + /** + * Returns true if node is dominated by dominator + */ + public boolean isDominatedBy(N node, N dominator) { + if (node == dominator) { + return true; + } + + // Walk up the dominator tree + N current = idoms.get(node); + while (current != null) { + if (current == dominator) { + return true; + } + current = idoms.get(current); + } + + return false; + } + + /** + * Gets the loop nesting depth of a node + */ + public int getLoopNestingDepth(N node, Map> naturalLoops) { + int depth = 0; + + for (Map.Entry> loop : naturalLoops.entrySet()) { + if (loop.getValue().contains(node)) { + depth++; + // Check if the loop header is contained in other loops + depth += getLoopNestingDepth(loop.getKey(), naturalLoops); + } + } + + return depth; + } + + /** + * Checks if a node is loop-invariant relative to a given loop + */ + public boolean isLoopInvariant(N node, Set loopBody) { + // A node is loop-invariant if: + // 1. It's not in the loop body, or + // 2. All its dependencies are loop-invariant + + if (!loopBody.contains(node)) { + return true; + } + + // Check if all predecessors are outside the loop or are the loop header + for (E predEdge : graph.getReverseEdges(node)) { + N pred = predEdge.src(); + if (loopBody.contains(pred) && !isDominatedBy(node, pred)) { + return false; + } + } + + return true; + } } diff --git a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/AbstractFastGraphTest.java b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/AbstractFastGraphTest.java index 5a0c636b..c452cc6a 100644 --- a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/AbstractFastGraphTest.java +++ b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/AbstractFastGraphTest.java @@ -6,9 +6,7 @@ import org.mapleir.stdlib.collections.graph.util.FakeFastEdge; import org.mapleir.stdlib.collections.graph.util.FakeFastVertex; -import junit.framework.TestCase; - -public abstract class AbstractFastGraphTest extends TestCase { +public abstract class AbstractFastGraphTest { private final boolean directed; protected final Map nodes = new HashMap<>(); diff --git a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/algorithms/DfsTest.java b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/algorithms/DfsTest.java index 15338f53..9cc54a01 100644 --- a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/algorithms/DfsTest.java +++ b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/algorithms/DfsTest.java @@ -5,6 +5,9 @@ import java.util.List; import java.util.Set; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mapleir.stdlib.collections.graph.util.GraphConverter; import org.mapleir.stdlib.collections.graph.util.OrderedNode; import org.mapleir.stdlib.collections.graph.util.OrderedNode.ODirectedGraph; @@ -13,14 +16,13 @@ import com.google.common.base.Predicates; import com.google.common.collect.Iterators; -import junit.framework.TestCase; +import static org.junit.jupiter.api.Assertions.*; - -public class DfsTest extends TestCase { +public class DfsTest { ODirectedGraph g; - @Override + @BeforeEach public void setUp() { try { g = (ODirectedGraph) GraphConverter.fromFile("/dfs.gv"); @@ -29,24 +31,28 @@ public void setUp() { } } + @Test public void testSimpleDfsPre() { DepthFirstSearch dfs = new SimpleDfs<>(g, getNode(g, 1), SimpleDfs.PRE); List res = dfs.getPreOrder(); assertPreOrdered(res); } + @Test public void testExtendedDfsPre() { DepthFirstSearch dfs = new ExtendedDfs<>(g, ExtendedDfs.PRE).run(getNode(g, 1)); List res = dfs.getPreOrder(); assertPreOrdered(res); } + @Test public void testSimpleDfsTopo() { DepthFirstSearch dfs = new SimpleDfs<>(g, getNode(g, 1), SimpleDfs.TOPO); List res = dfs.getTopoOrder(); assertTopoOrdered(res); } + @Test public void testExtendedDfsTopo() { DepthFirstSearch dfs = new ExtendedDfs<>(g, ExtendedDfs.TOPO).run(getNode(g, 1)); List res = dfs.getTopoOrder(); @@ -55,22 +61,22 @@ public void testExtendedDfsTopo() { private void assertPreOrdered(List nodes) { Set visited = new HashSet<>(); - assertEquals("missing nodes", new HashSet<>(nodes), g.vertices()); + assertEquals(new HashSet<>(nodes), g.vertices(), "missing nodes"); for (int i = 1; i < nodes.size(); i++) { OrderedNode node = nodes.get(i); visited.add(node); OrderedNode prev = nodes.get(i - 1); if (!Iterators.all(g.getSuccessors(prev).iterator(), Predicates.in(visited))) - assertTrue("unvisited pred", Iterators.contains(g.getPredecessors(node).iterator(), prev)); + assertTrue(Iterators.contains(g.getPredecessors(node).iterator(), prev)); } } private void assertTopoOrdered(List nodes) { Set visited = new HashSet<>(); - assertEquals("missing nodes", new HashSet<>(nodes), g.vertices()); + assertEquals(new HashSet<>(nodes), g.vertices()); for (OrderedNode node : nodes) { visited.add(node); - assertTrue("unvisited pred", Iterators.all(g.getPredecessors(node).iterator(), Predicates.in(visited))); + assertTrue(Iterators.all(g.getPredecessors(node).iterator(), Predicates.in(visited)), "unvisited pred"); } } diff --git a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/algorithms/ReducibleLoopTest.java b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/algorithms/ReducibleLoopTest.java index 448a6a96..7a0cc535 100644 --- a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/algorithms/ReducibleLoopTest.java +++ b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/algorithms/ReducibleLoopTest.java @@ -2,16 +2,20 @@ import java.io.IOException; +import org.junit.jupiter.api.Test; import org.mapleir.stdlib.collections.graph.AbstractFastGraphTest; import org.mapleir.stdlib.collections.graph.GraphUtils; import org.mapleir.stdlib.collections.graph.directed.FakeFastDirectedGraph; +import static org.junit.jupiter.api.Assertions.*; + public class ReducibleLoopTest extends AbstractFastGraphTest { public ReducibleLoopTest() { super(true); } + @Test public void testIrreducible1() throws IOException { FakeFastDirectedGraph g = new FakeFastDirectedGraph(); g.addVertex(node(1)); diff --git a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/directed/FastDirectedGraphTest.java b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/directed/FastDirectedGraphTest.java index 2ae2c0b6..1b7c0769 100644 --- a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/directed/FastDirectedGraphTest.java +++ b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/directed/FastDirectedGraphTest.java @@ -1,7 +1,9 @@ package org.mapleir.stdlib.collections.graph.directed; +import static org.junit.jupiter.api.Assertions.*; import static org.mapleir.stdlib.collections.graph.util.CollectionUtil.*; +import org.junit.jupiter.api.Test; import org.mapleir.stdlib.collections.graph.AbstractFastGraphTest; import org.mapleir.stdlib.collections.graph.util.FakeFastEdge; @@ -11,6 +13,7 @@ public FastDirectedGraphTest() { super(true); } + @Test public void testAddVertex() { FakeFastDirectedGraph g = graph(); assertEquals(0, g.size()); @@ -23,7 +26,8 @@ public void testAddVertex() { assertEquals(1, g.size()); // assertEquals(1, g.reverseSize()); } - + + @Test public void testAddVertexByAddEdge() { FakeFastDirectedGraph g = graph(); FakeFastEdge e = edge(1, 2); @@ -32,7 +36,8 @@ public void testAddVertexByAddEdge() { assertEquals(2, g.size()); // assertEquals(2, g.reverseSize()); } - + + @Test public void testAddEdge() { FakeFastDirectedGraph g = graph(); FakeFastEdge e = edge(1, 2); @@ -45,7 +50,8 @@ public void testAddEdge() { assertTrue(g.containsEdge(e)); assertTrue(g.containsReverseEdge(e)); } - + + @Test public void testRemoveEdge() { FakeFastDirectedGraph g = graph(); FakeFastEdge e = edge(1, 2); @@ -57,7 +63,8 @@ public void testRemoveEdge() { assertFalse(g.containsEdge(e)); assertFalse(g.containsReverseEdge(e)); } - + + @Test public void testRemoveVertex() { FakeFastDirectedGraph g = graph(); FakeFastEdge e1 = edge(1, 2), e2 = edge(1, 3), @@ -81,7 +88,8 @@ public void testRemoveVertex() { assertFalse(g.containsEdge(e3)); assertTrue(g.containsEdge(e4)); } - + + @Test public void testReplace() { FakeFastDirectedGraph g = graph(); FakeFastEdge e1 = edge(1, 2), e2 = edge(1, 3), e3 = edge(2, 4), e4 = edge(3, 4); @@ -102,6 +110,7 @@ public void testReplace() { } /* internal test */ + @Test public void testClone() { FakeFastDirectedGraph g = graph(); FakeFastEdge e1 = edge(1, 2); diff --git a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/dom/LT79DomLoopTest.java b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/dom/LT79DomLoopTest.java new file mode 100644 index 00000000..64eb5d0d --- /dev/null +++ b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/dom/LT79DomLoopTest.java @@ -0,0 +1,370 @@ +package org.mapleir.stdlib.collections.graph.dom; + +import org.junit.jupiter.api.Test; +import org.mapleir.stdlib.collections.graph.algorithms.LT79Dom; +import org.mapleir.stdlib.collections.graph.util.FakeFastEdge; +import org.mapleir.stdlib.collections.graph.util.FakeFastVertex; +import org.mapleir.stdlib.collections.graph.util.GraphTestBuilder; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; +import java.util.Set; + +public class LT79DomLoopTest { + + @Test + public void testSimpleLoop() { + String graphText = """ + A -> B + B -> C + C -> B + """; + + GraphTestBuilder builder = new GraphTestBuilder(graphText); + LT79Dom domTree = + new LT79Dom<>(builder.getGraph(), builder.getRoot()); + + Map> loops = domTree.findNaturalLoops(); + + assertEquals(1, loops.size(), "Should find one loop"); + assertTrue(loops.containsKey(builder.getVertex("B")), "B should be loop header"); + assertTrue(loops.get(builder.getVertex("B")).containsAll( + Set.of(builder.getVertex("B"), builder.getVertex("C"))), + "Loop should contain B and C"); + } + + @Test + public void testNestedLoops() { + String graphText = """ + A -> B + B -> C + C -> D + D -> B + D -> E + E -> B + """; + + GraphTestBuilder builder = new GraphTestBuilder(graphText); + LT79Dom domTree = + new LT79Dom<>(builder.getGraph(), builder.getRoot()); + + Map> loops = domTree.findNaturalLoops(); + + assertEquals(1, loops.size(), "Should find one loop"); + assertTrue(loops.containsKey(builder.getVertex("B")), "B should be loop header"); + + Set loopBody = loops.get(builder.getVertex("B")); + assertTrue(loopBody.containsAll(Set.of( + builder.getVertex("B"), + builder.getVertex("C"), + builder.getVertex("D"), + builder.getVertex("E") + )), "Loop should contain B, C, D, and E"); + } + + @Test + public void testSelfLoop() { + String graphText = """ + A -> B + B -> B + """; + + GraphTestBuilder builder = new GraphTestBuilder(graphText); + LT79Dom domTree = + new LT79Dom<>(builder.getGraph(), builder.getRoot()); + + Map> loops = domTree.findNaturalLoops(); + + assertEquals(1, loops.size(), "Should find one loop"); + assertTrue(loops.containsKey(builder.getVertex("B")), "B should be loop header"); + assertEquals( + Set.of(builder.getVertex("B")), + loops.get(builder.getVertex("B")), + "Self loop should contain only B" + ); + } + + @Test + public void testMultipleExitLoops() { + String graphText = """ + A -> B + B -> C, E + C -> D + D -> B, F + """; + + GraphTestBuilder builder = new GraphTestBuilder(graphText); + LT79Dom domTree = + new LT79Dom<>(builder.getGraph(), builder.getRoot()); + + Map> loops = domTree.findNaturalLoops(); + + assertTrue(loops.containsKey(builder.getVertex("B")), "B should be loop header"); + Set loopBody = loops.get(builder.getVertex("B")); + assertTrue(loopBody.containsAll(Set.of( + builder.getVertex("B"), + builder.getVertex("C"), + builder.getVertex("D") + )), "Loop should contain B, C, and D"); + assertFalse(loopBody.contains(builder.getVertex("E")), "Loop should not contain exit node E"); + assertFalse(loopBody.contains(builder.getVertex("F")), "Loop should not contain exit node F"); + } + + @Test + public void testComplexNestedLoops() { + String graphText = """ + A -> B + B -> C + C -> D, G + D -> E + E -> F + F -> D + G -> H + H -> G, B + """; + + GraphTestBuilder builder = new GraphTestBuilder(graphText); + LT79Dom domTree = + new LT79Dom<>(builder.getGraph(), builder.getRoot()); + + Map> loops = domTree.findNaturalLoops(); + + assertEquals(3, loops.size(), "Should find three loops"); + assertTrue(loops.containsKey(builder.getVertex("B")), "B should be loop header"); + assertTrue(loops.containsKey(builder.getVertex("D")), "D should be loop header"); + assertTrue(loops.containsKey(builder.getVertex("G")), "G should be loop header"); + } + + @Test + void testIrreducibleLoop() { + String graphText = """ + A -> B, C + B -> D + C -> D + D -> B, C + """; + + GraphTestBuilder builder = new GraphTestBuilder(graphText); + LT79Dom domTree = + new LT79Dom<>(builder.getGraph(), builder.getRoot()); + + Map> loops = domTree.findNaturalLoops(); + + loops.forEach((k, v) -> System.out.println(k + " -> " + v.stream().map(FakeFastVertex::toString).reduce("", (a, b) -> a + " " + b))); + + // Should identify both B and C as loop headers since both are entry points + assertTrue(loops.containsKey(builder.getVertex("B")), "B should be loop header"); + assertTrue(loops.containsKey(builder.getVertex("D")), "C should be loop header"); + + // Both loops should contain D as it's part of both cycles + assertTrue(loops.get(builder.getVertex("B")).contains(builder.getVertex("D")), + "B's loop should contain D"); + assertTrue(loops.get(builder.getVertex("D")).contains(builder.getVertex("D")), + "D's loop should contain D"); + } + + @Test + void testComplexLoopNesting() { + String graphText = """ + Entry -> A + A -> B + B -> C, Exit1 + C -> D, Exit2 + D -> E, F + E -> C + F -> G + G -> F, B, Exit3 + """; + + GraphTestBuilder builder = new GraphTestBuilder(graphText); + LT79Dom domTree = + new LT79Dom<>(builder.getGraph(), builder.getRoot()); + + Map> loops = domTree.findNaturalLoops(); + + // Should find three nested loops: B->G->B, C->E->C, and F->G->F + assertEquals(3, loops.size(), "Should find three loops"); + + // Check outer loop (B) + Set outerLoop = loops.get(builder.getVertex("B")); + assertNotNull(outerLoop, "B should be a loop header"); + assertTrue(outerLoop.containsAll(Set.of( + builder.getVertex("B"), + builder.getVertex("C"), + builder.getVertex("D"), + builder.getVertex("E"), + builder.getVertex("F"), + builder.getVertex("G") + )), "Outer loop should contain all inner loop nodes"); + + // Check middle loop (C) + Set middleLoop = loops.get(builder.getVertex("C")); + assertNotNull(middleLoop, "C should be a loop header"); + assertTrue(middleLoop.containsAll(Set.of( + builder.getVertex("C"), + builder.getVertex("D"), + builder.getVertex("E") + )), "Middle loop should contain C, D, and E"); + + // Check inner loop (F) + Set innerLoop = loops.get(builder.getVertex("F")); + assertNotNull(innerLoop, "F should be a loop header"); + assertTrue(innerLoop.containsAll(Set.of( + builder.getVertex("F"), + builder.getVertex("G") + )), "Inner loop should contain F and G"); + } + + @Test + void testLoopWithMultipleBackEdges() { + String graphText = """ + A -> B + B -> C + C -> D + D -> E + E -> B, C, D + """; + + GraphTestBuilder builder = new GraphTestBuilder(graphText); + LT79Dom domTree = + new LT79Dom<>(builder.getGraph(), builder.getRoot()); + + Map> loops = domTree.findNaturalLoops(); + + // Should find three loops due to multiple back edges from E + assertEquals(3, loops.size(), "Should find three loops"); + + // Check each loop + Set loopB = loops.get(builder.getVertex("B")); + Set loopC = loops.get(builder.getVertex("C")); + Set loopD = loops.get(builder.getVertex("D")); + + assertNotNull(loopB, "B should be a loop header"); + assertNotNull(loopC, "C should be a loop header"); + assertNotNull(loopD, "D should be a loop header"); + + // Check loop contents + assertTrue(loopB.containsAll(Set.of( + builder.getVertex("B"), + builder.getVertex("C"), + builder.getVertex("D"), + builder.getVertex("E") + )), "B's loop should contain all nodes"); + + assertTrue(loopC.containsAll(Set.of( + builder.getVertex("C"), + builder.getVertex("D"), + builder.getVertex("E") + )), "C's loop should contain C, D, and E"); + + assertTrue(loopD.containsAll(Set.of( + builder.getVertex("D"), + builder.getVertex("E") + )), "D's loop should contain D and E"); + } + + @Test + void testDisconnectedLoops() { + String graphText = """ + A -> B, X + B -> C + C -> B + X -> Y + Y -> Z + Z -> Y + """; + + GraphTestBuilder builder = new GraphTestBuilder(graphText); + LT79Dom domTree = + new LT79Dom<>(builder.getGraph(), builder.getRoot()); + + Map> loops = domTree.findNaturalLoops(); + + // Should find two separate loops + assertEquals(2, loops.size(), "Should find two loops"); + + // Check first loop + Set loopB = loops.get(builder.getVertex("B")); + assertNotNull(loopB, "B should be a loop header"); + assertTrue(loopB.containsAll(Set.of( + builder.getVertex("B"), + builder.getVertex("C") + )), "First loop should contain B and C"); + + // Check second loop + Set loopY = loops.get(builder.getVertex("Y")); + assertNotNull(loopY, "Y should be a loop header"); + assertTrue(loopY.containsAll(Set.of( + builder.getVertex("Y"), + builder.getVertex("Z") + )), "Second loop should contain Y and Z"); + } + + /** + * Example graph structure showing natural loops: + * + * A + * | + * v + * +---> B <---+ + * | | \ | + * | v v | + * | C D | + * | | | | + * | v v | + * | E ----+ + * | / \ + * F <-' v + * G + * | + * +-> C + * + * Contains two natural loops: + * 1. B -> F -> B (outer loop) + * 2. C -> G -> C (inner loop) + */ + @Test + void testLoopWithCrossEdges() { + String graphText = """ + A -> B + B -> C, D + C -> E + D -> E + E -> F, G + F -> B + G -> C + """; + + GraphTestBuilder builder = new GraphTestBuilder(graphText); + LT79Dom domTree = + new LT79Dom<>(builder.getGraph(), builder.getRoot()); + + Map> loops = domTree.findNaturalLoops(); + + loops.forEach((k, v) -> System.out.println(k + " -> " + v.stream().map(FakeFastVertex::toString).reduce("", (a, b) -> a + " " + b))); + // Should find two interleaved loops + assertEquals(2, loops.size(), "Should find two loops: B->F and C->G"); + + Set loopB = loops.get(builder.getVertex("B")); + Set loopC = loops.get(builder.getVertex("E")); + + assertNotNull(loopB, "B should be a loop header"); + assertNotNull(loopC, "E should be a loop header"); + + // Check that cross edges are handled correctly + assertTrue(loopB.containsAll(Set.of( + builder.getVertex("B"), + builder.getVertex("C"), + builder.getVertex("D"), + builder.getVertex("E"), + builder.getVertex("F") + )), "B's loop should contain all nodes except G"); + + assertTrue(loopC.containsAll(Set.of( + builder.getVertex("C"), + builder.getVertex("E"), + builder.getVertex("G") + )), "E's loop should contain C, E, and G"); + } +} \ No newline at end of file diff --git a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/undirected/FastUndirectedGraphTest.java b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/undirected/FastUndirectedGraphTest.java index 7c96107a..222c2d48 100644 --- a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/undirected/FastUndirectedGraphTest.java +++ b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/undirected/FastUndirectedGraphTest.java @@ -1,5 +1,6 @@ package org.mapleir.stdlib.collections.graph.undirected; +import static org.junit.jupiter.api.Assertions.*; import static org.mapleir.stdlib.collections.graph.util.CollectionUtil.*; import org.mapleir.stdlib.collections.graph.AbstractFastGraphTest; @@ -112,8 +113,8 @@ private void assertNotContainsBidiEdge(FakeFastUndirectedGraph g, FakeFastEdge e private > void assertSisterEdges(E e1, E e2) { String msg = getSisterErrorMsg(e1, e2); - assertEquals(msg, e1.src(), e2.dst()); - assertEquals(msg, e1.dst(), e2.src()); + assertEquals(e1.src(), e2.dst(), msg); + assertEquals(e1.dst(), e2.src(), msg); } private String getSisterErrorMsg(Object o1, Object o2) { diff --git a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/util/CollectionUtil.java b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/util/CollectionUtil.java index f1f300b1..1eb9c807 100644 --- a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/util/CollectionUtil.java +++ b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/util/CollectionUtil.java @@ -10,7 +10,7 @@ import org.mapleir.stdlib.collections.graph.FastGraphEdge; import org.mapleir.stdlib.collections.graph.FastGraphVertex; -import static junit.framework.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class CollectionUtil { diff --git a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/util/FakeFastVertex.java b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/util/FakeFastVertex.java index f53fcddf..a2efc2fa 100644 --- a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/util/FakeFastVertex.java +++ b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/util/FakeFastVertex.java @@ -3,19 +3,28 @@ import org.mapleir.stdlib.collections.graph.FastGraphVertex; public class FakeFastVertex implements FastGraphVertex { - private final int id; + private final String id; public FakeFastVertex(int id) { + this.id = String.valueOf(id); + } + + public FakeFastVertex(String id) { this.id = id; } @Override public int getNumericId() { - return id; + return id.hashCode(); } @Override public String getDisplayName() { - return String.valueOf(id); + return id; + } + + @Override + public String toString() { + return getDisplayName(); } } diff --git a/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/util/GraphTestBuilder.java b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/util/GraphTestBuilder.java new file mode 100644 index 00000000..ec5ffe89 --- /dev/null +++ b/org.mapleir.parent/org.mapleir.stdlib/src/test/java/org/mapleir/stdlib/collections/graph/util/GraphTestBuilder.java @@ -0,0 +1,94 @@ +package org.mapleir.stdlib.collections.graph.util; + +import org.mapleir.stdlib.collections.graph.FastDirectedGraph; +import org.mapleir.stdlib.collections.graph.FastGraphEdgeImpl; +import org.mapleir.stdlib.collections.graph.FastGraphVertex; +import org.mapleir.stdlib.collections.graph.directed.FakeFastDirectedGraph; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Framework for creating test graphs from text representations. + * Example usage: + *
+ * String graphText = """
+ *     A -> B, C
+ *     B -> C, D
+ *     C -> B
+ *     D -> A
+ * """;
+ * GraphTestBuilder builder = new GraphTestBuilder(graphText);
+ * 
+ */ +public class GraphTestBuilder { + private final FastDirectedGraph graph; + private final Map vertices; + private final FakeFastVertex root; + + /** + * Creates a graph from a text representation. + * Format: + * NodeName -> Node1, Node2, Node3 + * First node is considered the root. + */ + public GraphTestBuilder(String graphText) { + graph = new FakeFastDirectedGraph(); + vertices = new HashMap<>(); + + // Parse the graph text + String[] lines = graphText.trim().split("\n"); + Pattern pattern = Pattern.compile("(\\w+)\\s*->\\s*([\\w\\s,]+)"); + + FakeFastVertex firstVertex = null; + + for (String line : lines.clone()) { + line = line.trim(); + if (line.isEmpty()) continue; + + Matcher matcher = pattern.matcher(line); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid line format: " + line); + } + + String sourceName = matcher.group(1); + String[] targetNames = matcher.group(2).split("\\s*,\\s*"); + + FakeFastVertex source = getOrCreateVertex(sourceName); + if (firstVertex == null) { + firstVertex = source; + } + + for (String targetName : targetNames) { + FakeFastVertex target = getOrCreateVertex(targetName); + graph.addEdge(new FakeFastEdge(source, target, true)); + } + } + + this.root = firstVertex; + if (this.root == null) { + throw new IllegalArgumentException("Graph must have at least one vertex"); + } + } + + private FakeFastVertex getOrCreateVertex(String name) { + return vertices.computeIfAbsent(name, n -> { + FakeFastVertex v = new FakeFastVertex(n); + graph.addVertex(v); + return v; + }); + } + + public FastDirectedGraph getGraph() { + return graph; + } + + public FakeFastVertex getRoot() { + return root; + } + + public FakeFastVertex getVertex(String name) { + return vertices.get(name); + } +} \ No newline at end of file