From ab5c4bfd82d7cf7600a72ea2f873140d6c96022f Mon Sep 17 00:00:00 2001 From: Shanyu Thibaut Juneja Date: Tue, 17 Dec 2024 22:10:51 -0500 Subject: [PATCH] fix: more works on new exclusion system --- .../build.gradle | 8 +- .../obfuscator/exempt/Exclusion.java | 16 +- .../obfuscator/exempt/ExclusionHelper.java | 2 +- .../obfuscator/exempt/v2/ExclusionParser.java | 273 +++++++++++++----- .../obfuscator/util/JdkDownloader.java | 5 +- .../test/exclusion/NestedExclusionTest.java | 195 +++++++++++++ .../main/java/org/mapleir/asm/MethodNode.java | 1 + 7 files changed, 420 insertions(+), 80 deletions(-) create mode 100644 dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/exclusion/NestedExclusionTest.java diff --git a/dev.skidfuscator.obfuscator.pureanalysis/build.gradle b/dev.skidfuscator.obfuscator.pureanalysis/build.gradle index 44296a9..eaddf30 100644 --- a/dev.skidfuscator.obfuscator.pureanalysis/build.gradle +++ b/dev.skidfuscator.obfuscator.pureanalysis/build.gradle @@ -13,10 +13,10 @@ repositories { dependencies { api project(':modasm') - //api 'io.github.terminalsin:SSVM:dev-SNAPSHOT' - api 'dev.xdark:ssvm-core:2.0.2' - api 'dev.xdark:ssvm-invoke:2.0.2' - api 'dev.xdark:ssvm-io:2.0.2' + api 'io.github.terminalsin:SSVM:1.0.0-SNAPSHOT' + //api 'dev.xdark:ssvm-core:2.0.2' + //api 'dev.xdark:ssvm-invoke:2.0.2' + //api 'dev.xdark:ssvm-io:2.0.2' testImplementation platform('org.junit:junit-bom:5.10.0') testImplementation 'org.junit.jupiter:junit-jupiter' diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/exempt/Exclusion.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/exempt/Exclusion.java index b0bfee5..721c582 100644 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/exempt/Exclusion.java +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/exempt/Exclusion.java @@ -13,7 +13,8 @@ @Data @Builder public class Exclusion { - private ExclusionMap testers; + private final ExclusionMap testers; + private final boolean include; /** * Test if a class is to be excluded @@ -24,7 +25,8 @@ public class Exclusion { public boolean test(final ClassNode classNode) { assert testers.containsKey(ExclusionType.CLASS) : "Trying to test with null class tester"; - return testers.poll(ExclusionType.CLASS).test(classNode); + boolean result = testers.poll(ExclusionType.CLASS).test(classNode); + return include ? !result : result; } /** @@ -34,11 +36,12 @@ public boolean test(final ClassNode classNode) { * @return the boolean */ public boolean test(final MethodNode methodNode) { - if (test(methodNode.getOwnerClass())) + if (test(methodNode.getOwnerClass()) != include) return true; assert testers.containsKey(ExclusionType.METHOD) : "Trying to test with null method tester"; - return testers.poll(ExclusionType.METHOD).test(methodNode); + boolean result = testers.poll(ExclusionType.METHOD).test(methodNode); + return include != result; } /** @@ -48,11 +51,12 @@ public boolean test(final MethodNode methodNode) { * @return the boolean */ public boolean test(final FieldNode fieldNode) { - if (test(fieldNode.getOwnerClass())) + if (test(fieldNode.getOwnerClass()) != include) return true; assert testers.containsKey(ExclusionType.FIELD) : "Trying to test with null field tester"; - return testers.poll(ExclusionType.FIELD).test(fieldNode); + boolean result = testers.poll(ExclusionType.FIELD).test(fieldNode); + return include != result; } @Override diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/exempt/ExclusionHelper.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/exempt/ExclusionHelper.java index 04c413a..de28e82 100644 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/exempt/ExclusionHelper.java +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/exempt/ExclusionHelper.java @@ -223,6 +223,6 @@ public String toString() { }); } - return new Exclusion(map); + return new Exclusion(map, false); } } \ No newline at end of file diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/exempt/v2/ExclusionParser.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/exempt/v2/ExclusionParser.java index 632fad7..51b08db 100644 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/exempt/v2/ExclusionParser.java +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/exempt/v2/ExclusionParser.java @@ -39,6 +39,20 @@ private static class ParsedMember { private final String name; // member name private final List parameters; // method parameters private final String containingClass; // class this member belongs to + private final boolean isInclusion; // whether this member is an inclusion + + @Override + public String toString() { + return "ParsedMember{" + + "type='" + type + '\'' + + ", modifiers=" + modifiers + + ", returnType='" + returnType + '\'' + + ", name='" + name + '\'' + + ", parameters=" + parameters + + ", containingClass='" + containingClass + '\'' + + ", isInclusion=" + isInclusion + + '}'; + } } @Data @@ -49,6 +63,7 @@ private static class ParsedClass { private final String extendsClass; // extended class private final Set interfaces; // implemented interfaces private final List members; // class members + private final boolean isInclude; // whether this class is an inclusion } /** @@ -72,14 +87,28 @@ private ParsedClass parseClass(String input) { throw new ExclusionParseException("Empty input"); } - // Parse class header - String header = input; - int braceIndex = header.indexOf("{"); + // Parse class header and body separately + String header; + String body = ""; + int braceIndex = input.indexOf("{"); if (braceIndex != -1) { - header = header.substring(0, braceIndex).trim(); + header = input.substring(0, braceIndex).trim(); + int closingBrace = input.lastIndexOf("}"); + if (closingBrace != -1) { + body = input.substring(braceIndex + 1, closingBrace).trim(); + } + } else { + header = input.trim(); + } + + // Check for inclusion prefix + boolean isInclusion = header.startsWith("!"); + if (isInclusion) { + header = header.substring(1); } + if (!header.startsWith("@")) { - throw new ExclusionParseException("Class declaration must start with @"); + throw new ExclusionParseException("Class declaration must start with @ or !@"); } // Split header into tokens @@ -141,17 +170,18 @@ private ParsedClass parseClass(String input) { // Parse members using state machine List members = new ArrayList<>(); - if (braceIndex != -1) { - MemberParserState state = new MemberParserState(input.substring(braceIndex + 1, input.lastIndexOf("}")).trim()); + if (!body.isEmpty()) { + MemberParserState state = new MemberParserState(body); while (state.hasMore()) { ParsedMember member = state.parseNextMember(className); if (member != null) { + System.out.println("member: " + member); members.add(member); } } } - return new ParsedClass(classType, modifiers, className, extendsClass, interfaces, members); + return new ParsedClass(classType, modifiers, className, extendsClass, interfaces, members, isInclusion); } private static class MemberParserState { @@ -159,10 +189,13 @@ private static class MemberParserState { private int position; private StringBuilder currentToken; private ParserState state; + private int braceDepth; + private boolean isInclusion; private enum ParserState { LOOKING_FOR_AT, READING_MEMBER, + READING_NESTED_CLASS, SKIPPING_WHITESPACE } @@ -171,6 +204,8 @@ public MemberParserState(String input) { this.position = 0; this.currentToken = new StringBuilder(); this.state = ParserState.LOOKING_FOR_AT; + this.braceDepth = 0; + this.isInclusion = false; } public boolean hasMore() { @@ -179,13 +214,18 @@ public boolean hasMore() { public ParsedMember parseNextMember(String className) { currentToken.setLength(0); + isInclusion = false; while (hasMore()) { char c = input.charAt(position); - switch (state) { case LOOKING_FOR_AT: - if (c == '@') { + if (c == '!') { + isInclusion = true; + position++; + System.out.println("isInclusion: " + isInclusion); + continue; + } else if (c == '@') { state = ParserState.READING_MEMBER; position++; } else if (!Character.isWhitespace(c)) { @@ -196,17 +236,28 @@ public ParsedMember parseNextMember(String className) { break; case READING_MEMBER: - if (c == '@') { + if (c == '@' || c == '!') { // Found start of next member String memberDecl = currentToken.toString().trim(); + System.out.println("memberDecl: " + memberDecl); if (!memberDecl.isEmpty()) { - return parseMember(memberDecl, className, -1); + return createParsedMember(memberDecl, className); } - state = ParserState.READING_MEMBER; + position--; // Back up to reprocess the @ or ! + state = ParserState.LOOKING_FOR_AT; + } else if (c == '\n' || c == ';') { + // End of member declaration + String memberDecl = currentToken.toString().trim(); + if (!memberDecl.isEmpty()) { + state = ParserState.LOOKING_FOR_AT; + position++; + return createParsedMember(memberDecl, className); + } + position++; } else { currentToken.append(c); + position++; } - position++; break; case SKIPPING_WHITESPACE: @@ -222,24 +273,44 @@ public ParsedMember parseNextMember(String className) { // Handle last member if exists String memberDecl = currentToken.toString().trim(); if (!memberDecl.isEmpty()) { - return parseMember(memberDecl, className, -1); + return createParsedMember(memberDecl, className); } return null; } + + private ParsedMember createParsedMember(String declaration, String className) { + return parseMember(declaration, className, -1, isInclusion); + } + + private ParsedMember createNestedClassMember(String declaration, String className) { + // Parse the nested class + ParsedClass nestedClass = parseClass(declaration); + + // Convert to a special member + return new ParsedMember( + "class", + nestedClass.getModifiers(), + null, + nestedClass.getName(), + Collections.emptyList(), + className, + isInclusion || nestedClass.isInclude() + ); + } } /** * Parses a member (method or field) declaration */ - private ParsedMember parseMember(String input, String containingClass, int lineNum) { + private ParsedMember parseMember(String input, String containingClass, int lineNum, boolean inclusion) { List tokens = tokenize(input); if (tokens.isEmpty()) { throw new ExclusionParseException("Invalid member declaration", lineNum); } String memberType = tokens.get(0); - if (!memberType.equals("method") && !memberType.equals("field")) { + if (!memberType.equals("method") && !memberType.equals("field") && !memberType.equals("class")) { throw new ExclusionParseException("Invalid member type: " + memberType, lineNum); } tokens.remove(0); @@ -271,7 +342,7 @@ private ParsedMember parseMember(String input, String containingClass, int lineN throw new ExclusionParseException("Member name is required", lineNum); } - return new ParsedMember(memberType, modifiers, returnType, name, parameters, containingClass); + return new ParsedMember(memberType, modifiers, returnType, name, parameters, containingClass, inclusion); } /** @@ -571,7 +642,24 @@ public boolean test(ClassNode var) { } } - return regex.matcher(var.getName()).find(); + boolean initialNameMatch = regex.matcher(var.getName()).find() != parsedClass.isInclude(); + + for (ParsedMember member : parsedClass.getMembers()) { + if (!member.getType().equalsIgnoreCase("class")) + continue; + + final boolean match = Pattern + .compile(convertClassPattern(parsedClass.getName() + member.getName())) + .matcher(var.getName()) + .find(); + + + if (match) { + initialNameMatch = match != member.isInclusion(); + } + } + + return initialNameMatch; } @Override @@ -582,72 +670,123 @@ public String toString() { } private void generateMethodTesters(ExclusionMap map, ParsedClass parsedClass) { - List> methodTesters = new ArrayList<>(); + final Pattern classRegex = Pattern.compile(convertClassPattern(parsedClass.getName())); + final List includedClassPatterns = new ArrayList<>(); + final List methodPatterns = new ArrayList<>(); + // Collect patterns from members for (ParsedMember member : parsedClass.getMembers()) { - if (!member.getType().equals("method")) continue; - - final Pattern nameRegex = Pattern.compile(member.getName().replace("*", ".*")); - final Pattern descRegex = member.getReturnType() != null ? - Pattern.compile(member.getReturnType()) : null; - - methodTesters.add(new ExclusionTester() { - @Override - public boolean test(MethodNode var) { - // Check basic modifiers - //System.out.println("testing: " + var.getName()); - //System.out.println("modifiers: " + parsedClass.getModifiers()); - final boolean initialMatch = Match.of("") - .match("static", var.isStatic()) - .match("public", var.isPublic()) - .match("protected", var.isProtected()) - .match("private", var.isPrivate()) - .check(); - - if (!initialMatch) return false; - - // Check specific modifiers - for (String modifier : member.getModifiers()) { - switch (modifier) { - case "public": if (!var.isPublic()) return false; break; - case "private": if (!var.isPrivate()) return false; break; - case "protected": if (!var.isProtected()) return false; break; - case "static": if (!var.isStatic()) return false; break; - case "final": if (!var.isFinal()) return false; break; - case "abstract": if (!var.isAbstract()) return false; break; - } - } - - // Check return type if specified - if (descRegex != null && !descRegex.matcher(var.getDesc()).lookingAt()) { - return false; + switch (member.getType()) { + case "class" -> { + if (member.isInclusion()) { + includedClassPatterns.add(Pattern.compile(convertClassPattern(member.getName()))); } - - // Check method name - return nameRegex.matcher(var.getName()).lookingAt(); } - - @Override - public String toString() { - return nameRegex.pattern(); + case "method" -> { + methodPatterns.add(new MethodPattern( + member.getName().replace("*", ".*"), + member.getModifiers(), + member.getReturnType(), + member.getParameters(), + member.isInclusion() + )); } - }); + default -> throw new ExclusionParseException("Invalid member type: " + member.getType()); + } } - // Combine all method testers into one map.put(ExclusionType.METHOD, new ExclusionTester() { @Override public boolean test(MethodNode var) { - return methodTesters.stream().anyMatch(tester -> tester.test(var)); + String ownerName = var.getOwnerClass().getName(); + + // First check if the method's class matches any inclusion pattern + for (Pattern includePattern : includedClassPatterns) { + if (includePattern.matcher(ownerName).find()) { + return false; // Include methods from included classes + } + } + + // Check if the method matches any specific method patterns + for (MethodPattern pattern : methodPatterns) { + if (pattern.matches(var)) { + return !pattern.isInclusion(); // Return false for inclusions, true for exclusions + } + } + + return false; } @Override public String toString() { - return "CombinedMethodTester"; + return "MethodTester[class=" + classRegex.pattern() + ", patterns=" + methodPatterns.size() + "]"; } }); } + private static class MethodPattern { + private final Pattern namePattern; + private final Set modifiers; + private final String returnType; + private final List parameters; + private final boolean inclusion; + + public MethodPattern(String name, Set modifiers, String returnType, List parameters, boolean inclusion) { + this.namePattern = Pattern.compile(name); + this.modifiers = modifiers; + this.returnType = returnType; + this.parameters = parameters; + this.inclusion = inclusion; + } + + public boolean matches(MethodNode method) { + // Check name pattern + if (!namePattern.matcher(method.getDisplayName()).matches()) { + return false; + } + + // Check modifiers if specified + for (String modifier : modifiers) { + switch (modifier) { + case "public": if (!method.isPublic()) return false; break; + case "private": if (!method.isPrivate()) return false; break; + case "protected": if (!method.isProtected()) return false; break; + case "static": if (!method.isStatic()) return false; break; + case "final": if (!method.isFinal()) return false; break; + case "abstract": if (!method.isAbstract()) return false; break; + case "native": if (!method.isNative()) return false; break; + } + } + + // Check return type if specified + if (returnType != null) { + if (!Pattern.compile(returnType).matcher(method.getDesc()).find()) { + return false; + } + } + + // Check parameters if specified + if (!parameters.isEmpty()) { + // TODO: Add parameter matching logic + // This would require parsing the method descriptor + } + + return true; + } + + public boolean isInclusion() { + return inclusion; + } + + @Override + public String toString() { + return (inclusion ? "!" : "") + namePattern.pattern() + + (modifiers.isEmpty() ? "" : " " + modifiers) + + (returnType != null ? " returns " + returnType : "") + + (parameters.isEmpty() ? "" : " params " + parameters); + } + } + private void generateFieldTesters(ExclusionMap map, ParsedClass parsedClass) { List> fieldTesters = new ArrayList<>(); diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/util/JdkDownloader.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/util/JdkDownloader.java index 35c2590..be359ce 100644 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/util/JdkDownloader.java +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/util/JdkDownloader.java @@ -30,7 +30,7 @@ public class JdkDownloader { break; case "windows": case "windows 11": - JDK_URL = "https://download.java.net/java/GA/jdk17.0.2/0d483333a00540d886896a45e7e18309295e7f3a/jdk-17.0.2_windows-x64_bin.zip"; + JDK_URL = "https://corretto.aws/downloads/resources/17.0.13.11.1/amazon-corretto-17.0.13.11.1-windows-x64-jdk.zip"; break; default: throw new IllegalStateException("Unsupported OS: " + OS); @@ -51,7 +51,8 @@ public static Path getJdkHome() throws IOException { cacheName = "amazon-corretto-17.jdk"; break; case "windows": - cacheName = "jdk-17.0.2"; + case "windows 11": + cacheName = "jdk17.0.13_11"; break; default: throw new IllegalStateException("Unsupported OS: " + OS); diff --git a/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/exclusion/NestedExclusionTest.java b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/exclusion/NestedExclusionTest.java new file mode 100644 index 0000000..bbfd8be --- /dev/null +++ b/dev.skidfuscator.obfuscator/src/test/java/dev/skidfuscator/test/exclusion/NestedExclusionTest.java @@ -0,0 +1,195 @@ +package dev.skidfuscator.test.exclusion; + +import dev.skidfuscator.obfuscator.exempt.Exclusion; +import dev.skidfuscator.obfuscator.exempt.SimpleExemptAnalysis; +import dev.skidfuscator.obfuscator.exempt.v2.ExclusionParser; +import dev.skidfuscator.obfuscator.transform.Transformer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mapleir.asm.ClassNode; +import org.mapleir.asm.MethodNode; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class NestedExclusionTest { + private SimpleExemptAnalysis analysis; + private Exclusion exclusion; + private ClassNode mockServiceClass; + private ClassNode mockInternalClass; + private ClassNode mockCustomService; + private ClassNode mockOtherService; + + @BeforeEach + void setUp() { + // Initialize the exempt analysis + analysis = new SimpleExemptAnalysis(null); + + // Parse and add the exclusion pattern + String pattern = """ + @class com.example.service.* { + !@class internal.* + !@class CustomService + } + """; + exclusion = ExclusionParser.parsePatternExclusion(pattern); + analysis.add(exclusion); + + // Setup mock classes + mockServiceClass = mock(ClassNode.class); + when(mockServiceClass.getName()).thenReturn("com/example/service/UserService"); + + mockInternalClass = mock(ClassNode.class); + when(mockInternalClass.getName()).thenReturn("com/example/service/internal/InternalService"); + + mockCustomService = mock(ClassNode.class); + when(mockCustomService.getName()).thenReturn("com/example/service/CustomService"); + + mockOtherService = mock(ClassNode.class); + when(mockOtherService.getName()).thenReturn("com/example/other/OtherService"); + } + + @Test + @DisplayName("Test general service package exclusion") + void testServicePackageExclusion() { + assertTrue(analysis.isExempt(mockServiceClass), + "Classes in service package should be excluded"); + } + + @Test + @DisplayName("Test internal package inclusion") + void testInternalPackageInclusion() { + assertFalse(analysis.isExempt(mockInternalClass), + "Classes in internal package should be included"); + } + + @Test + @DisplayName("Test CustomService inclusion") + void testCustomServiceInclusion() { + assertFalse(analysis.isExempt(mockCustomService), + "CustomService should be included"); + } + + @Test + @DisplayName("Test other package non-exclusion") + void testOtherPackageNonExclusion() { + assertFalse(analysis.isExempt(mockOtherService), + "Classes outside service package should not be excluded"); + } + + @Test + @DisplayName("Test method exclusions follow class rules") + void testMethodExclusions() { + // Create mock methods + MethodNode serviceMethod = mock(MethodNode.class); + when(serviceMethod.getOwnerClass()).thenReturn(mockServiceClass); + + MethodNode internalMethod = mock(MethodNode.class); + when(internalMethod.getOwnerClass()).thenReturn(mockInternalClass); + + MethodNode customServiceMethod = mock(MethodNode.class); + when(customServiceMethod.getOwnerClass()).thenReturn(mockCustomService); + + // Test method exclusions + assertTrue(analysis.isExempt(serviceMethod), + "Methods in service package should be excluded"); + + System.out.println(internalMethod.getOwnerClass().getName()); + System.out.println(exclusion); + assertFalse(analysis.isExempt(internalMethod), + "Methods in internal package should be included"); + + assertFalse(analysis.isExempt(customServiceMethod), + "Methods in CustomService should be included"); + } + + @Test + @DisplayName("Test cached results remain consistent") + void testCachedResults() { + // First check + boolean firstServiceCheck = analysis.isExempt(mockServiceClass); + boolean firstInternalCheck = analysis.isExempt(mockInternalClass); + boolean firstCustomCheck = analysis.isExempt(mockCustomService); + + // Second check (should use cache) + boolean secondServiceCheck = analysis.isExempt(mockServiceClass); + boolean secondInternalCheck = analysis.isExempt(mockInternalClass); + boolean secondCustomCheck = analysis.isExempt(mockCustomService); + + // Assert consistency + assertEquals(firstServiceCheck, secondServiceCheck, + "Cached service class result should be consistent"); + assertEquals(firstInternalCheck, secondInternalCheck, + "Cached internal class result should be consistent"); + assertEquals(firstCustomCheck, secondCustomCheck, + "Cached CustomService result should be consistent"); + } + + @Test + @DisplayName("Test direct method addition respects rules") + void testDirectMethodAddition() { + // Create methods + MethodNode serviceMethod = mock(MethodNode.class); + when(serviceMethod.getOwnerClass()).thenReturn(mockServiceClass); + + MethodNode internalMethod = mock(MethodNode.class); + when(internalMethod.getOwnerClass()).thenReturn(mockInternalClass); + + // Add methods directly + analysis.add(serviceMethod); + //analysis.add(internalMethod); + + // Verify they follow exclusion rules + assertTrue(analysis.isExempt(serviceMethod), + "Directly added service method should be excluded"); + assertFalse(analysis.isExempt(internalMethod), + "Directly added internal method should be included"); + } + + @Test + @DisplayName("Test nested inclusion/exclusion rules") + void testNestedInclusionExclusion() { + String pattern = """ + @class com.example.service.* { + !@class internal.* + !@class CustomService + } + """; + + Exclusion exclusion = ExclusionParser.parsePatternExclusion(pattern); + System.out.println(exclusion); + SimpleExemptAnalysis analysis = new SimpleExemptAnalysis(null); + analysis.add(exclusion); + + // Test regular service class (should be excluded) + ClassNode regularService = mock(ClassNode.class); + when(regularService.getName()).thenReturn("com/example/service/RegularService"); + assertTrue(analysis.isExempt(regularService)); + + // Test internal service class (should be included) + ClassNode internalService = mock(ClassNode.class); + when(internalService.getName()).thenReturn("com/example/service/internal/InternalService"); + assertFalse(analysis.isExempt(internalService)); + + // Test CustomService (should be included) + ClassNode customService = mock(ClassNode.class); + when(customService.getName()).thenReturn("com/example/service/CustomService"); + assertFalse(analysis.isExempt(customService)); + } + + @Test + @DisplayName("Test direct inclusion rule") + void testDirectInclusion() { + String pattern = "!@class com.example.include.*"; + + Exclusion exclusion = ExclusionParser.parsePatternExclusion(pattern); + SimpleExemptAnalysis analysis = new SimpleExemptAnalysis(null); + analysis.add(exclusion); + + ClassNode includedClass = mock(ClassNode.class); + when(includedClass.getName()).thenReturn("com/example/include/TestClass"); + assertFalse(analysis.isExempt(includedClass)); + } +} \ No newline at end of file diff --git a/org.mapleir.parent/org.mapleir.modasm/src/main/java/org/mapleir/asm/MethodNode.java b/org.mapleir.parent/org.mapleir.modasm/src/main/java/org/mapleir/asm/MethodNode.java index 75e96ce..d82d492 100644 --- a/org.mapleir.parent/org.mapleir.modasm/src/main/java/org/mapleir/asm/MethodNode.java +++ b/org.mapleir.parent/org.mapleir.modasm/src/main/java/org/mapleir/asm/MethodNode.java @@ -116,6 +116,7 @@ public boolean isClinit() { return this.getName().equals(""); } + public Type getOwnerType() { return Type.getType("L" + owner.getName() + ";"); }