diff options
| author | 2020-10-02 14:55:22 +0200 | |
|---|---|---|
| committer | 2020-10-02 14:55:22 +0200 | |
| commit | 112a49dccb1fe7792366112bc829352462cd298c (patch) | |
| tree | cfdd8f3af8744abc52581383e7f4cbbdc9f3aebe | |
| parent | Don't save duplicate messages in validation context (diff) | |
| parent | allow name clashes to pass where javac would accept them (diff) | |
| download | enigma-112a49dccb1fe7792366112bc829352462cd298c.tar.gz enigma-112a49dccb1fe7792366112bc829352462cd298c.tar.xz enigma-112a49dccb1fe7792366112bc829352462cd298c.zip | |
Merge pull request #299 from thiakil/validation-changes
Validation improvements
7 files changed, 258 insertions, 27 deletions
diff --git a/enigma/build.gradle b/enigma/build.gradle index 7cebcd16..8d081457 100644 --- a/enigma/build.gradle +++ b/enigma/build.gradle | |||
| @@ -53,7 +53,7 @@ file('src/test/java/cuchaz/enigma/inputs').listFiles().each { theFile -> | |||
| 53 | 53 | ||
| 54 | args '@src/test/resources/proguard-test.conf', '-injars', file('build/test-inputs/' + | 54 | args '@src/test/resources/proguard-test.conf', '-injars', file('build/test-inputs/' + |
| 55 | "${theFile.name}.jar"), '-libraryjars', libraryJarsArg, | 55 | "${theFile.name}.jar"), '-libraryjars', libraryJarsArg, |
| 56 | '-outjars', file('build/test-obf/' + "${theFile.name}.jar") | 56 | '-outjars', file('build/test-obf/' + "${theFile.name}.jar"), '-printmapping', file("build/test-obf/${theFile.name}-mapping.txt") |
| 57 | } | 57 | } |
| 58 | 58 | ||
| 59 | test.dependsOn "${theFile.name}TestObf" | 59 | test.dependsOn "${theFile.name}TestObf" |
diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java index f9f3b88e..134c05ce 100644 --- a/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java | |||
| @@ -1,15 +1,19 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | 1 | package cuchaz.enigma.translation.mapping; |
| 2 | 2 | ||
| 3 | import java.util.Collection; | 3 | import java.util.Collection; |
| 4 | import java.util.HashSet; | 4 | import java.util.Objects; |
| 5 | import java.util.Set; | ||
| 5 | import java.util.stream.Collectors; | 6 | import java.util.stream.Collectors; |
| 6 | 7 | ||
| 7 | import cuchaz.enigma.analysis.index.InheritanceIndex; | 8 | import cuchaz.enigma.analysis.index.InheritanceIndex; |
| 8 | import cuchaz.enigma.analysis.index.JarIndex; | 9 | import cuchaz.enigma.analysis.index.JarIndex; |
| 9 | import cuchaz.enigma.translation.Translator; | 10 | import cuchaz.enigma.translation.Translator; |
| 10 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | 11 | import cuchaz.enigma.translation.mapping.tree.EntryTree; |
| 12 | import cuchaz.enigma.translation.representation.AccessFlags; | ||
| 11 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | 13 | import cuchaz.enigma.translation.representation.entry.ClassEntry; |
| 14 | import cuchaz.enigma.translation.representation.entry.DefEntry; | ||
| 12 | import cuchaz.enigma.translation.representation.entry.Entry; | 15 | import cuchaz.enigma.translation.representation.entry.Entry; |
| 16 | import cuchaz.enigma.translation.representation.entry.FieldEntry; | ||
| 13 | import cuchaz.enigma.utils.validation.Message; | 17 | import cuchaz.enigma.utils.validation.Message; |
| 14 | import cuchaz.enigma.utils.validation.ValidationContext; | 18 | import cuchaz.enigma.utils.validation.ValidationContext; |
| 15 | 19 | ||
| @@ -35,18 +39,16 @@ public class MappingValidator { | |||
| 35 | 39 | ||
| 36 | private void validateUnique(ValidationContext vc, Entry<?> entry, String name) { | 40 | private void validateUnique(ValidationContext vc, Entry<?> entry, String name) { |
| 37 | ClassEntry containingClass = entry.getContainingClass(); | 41 | ClassEntry containingClass = entry.getContainingClass(); |
| 38 | Collection<ClassEntry> relatedClasses = getRelatedClasses(containingClass); | 42 | InheritanceIndex inheritanceIndex = index.getInheritanceIndex(); |
| 39 | |||
| 40 | for (ClassEntry relatedClass : relatedClasses) { | ||
| 41 | Entry<?> relatedEntry = entry.replaceAncestor(containingClass, relatedClass); | ||
| 42 | Entry<?> translatedEntry = deobfuscator.translate(relatedEntry); | ||
| 43 | |||
| 44 | Collection<Entry<?>> translatedSiblings = obfToDeobf.getSiblings(relatedEntry).stream() | ||
| 45 | .map(deobfuscator::translate) | ||
| 46 | .collect(Collectors.toList()); | ||
| 47 | 43 | ||
| 48 | if (!isUnique(translatedEntry, translatedSiblings, name)) { | 44 | //Step 1, check it's unique within its own siblings |
| 49 | Entry<?> parent = translatedEntry.getParent(); | 45 | Collection<Entry<?>> directTranslatedSiblings = obfToDeobf.getSiblings(entry).stream() |
| 46 | .map(deobfuscator::translate) | ||
| 47 | .collect(Collectors.toList()); | ||
| 48 | for (Entry<?> sibling : directTranslatedSiblings) { | ||
| 49 | if (entry.canConflictWith(sibling) && sibling.getName().equals(name) && isSynthetic(entry) == isSynthetic(sibling)) { | ||
| 50 | // allow clash if one is synthetic and the other is not | ||
| 51 | Entry<?> parent = entry.getParent(); | ||
| 50 | if (parent != null) { | 52 | if (parent != null) { |
| 51 | vc.raise(Message.NONUNIQUE_NAME_CLASS, name, parent); | 53 | vc.raise(Message.NONUNIQUE_NAME_CLASS, name, parent); |
| 52 | } else { | 54 | } else { |
| @@ -54,26 +56,88 @@ public class MappingValidator { | |||
| 54 | } | 56 | } |
| 55 | } | 57 | } |
| 56 | } | 58 | } |
| 57 | } | ||
| 58 | 59 | ||
| 59 | private Collection<ClassEntry> getRelatedClasses(ClassEntry classEntry) { | 60 | //Step 2, check ancestors, ignoring members invisible to children |
| 60 | InheritanceIndex inheritanceIndex = index.getInheritanceIndex(); | 61 | Set<ClassEntry> ancestors = inheritanceIndex.getAncestors(containingClass); |
| 62 | for (ClassEntry ancestor : ancestors) { | ||
| 63 | Entry<?> reparentedEntry = entry.replaceAncestor(containingClass, ancestor); | ||
| 64 | Entry<?> translatedEntry = Objects.requireNonNull(deobfuscator.translate(reparentedEntry), "Translation failed"); | ||
| 65 | Collection<Entry<?>> translatedSiblings = obfToDeobf.getSiblings(reparentedEntry).stream() | ||
| 66 | .filter(it->!entry.equals(it))//e.g. for root classes, ensure we dont match the name against itself | ||
| 67 | .filter(this::isVisibleToChildren) | ||
| 68 | .collect(Collectors.toList()); | ||
| 69 | for (Entry<?> parentSibling : translatedSiblings) { | ||
| 70 | Entry<?> parentSiblingTranslated = Objects.requireNonNull(deobfuscator.translate(parentSibling), "Translation failed"); | ||
| 71 | if (translatedEntry.canConflictWith(parentSiblingTranslated) && parentSiblingTranslated.getName().equals(name) && !isAcceptableOverride(parentSibling, entry)) { | ||
| 72 | Entry<?> parent = translatedEntry.getParent(); | ||
| 73 | if (parent != null) { | ||
| 74 | vc.raise(Message.NONUNIQUE_NAME_CLASS, name, parent); | ||
| 75 | } else { | ||
| 76 | vc.raise(Message.NONUNIQUE_NAME, name); | ||
| 77 | } | ||
| 78 | } | ||
| 79 | } | ||
| 80 | } | ||
| 61 | 81 | ||
| 62 | Collection<ClassEntry> relatedClasses = new HashSet<>(); | 82 | //Step 3, if this entry is visible to children, see if it clashes with any of their names |
| 63 | relatedClasses.add(classEntry); | 83 | if (isVisibleToChildren(entry)) { |
| 64 | relatedClasses.addAll(inheritanceIndex.getChildren(classEntry)); | 84 | Collection<ClassEntry> children = inheritanceIndex.getDescendants(containingClass); |
| 65 | relatedClasses.addAll(inheritanceIndex.getAncestors(classEntry)); | 85 | for (ClassEntry child : children) { |
| 86 | Entry<?> reparentedEntry = entry.replaceAncestor(containingClass, child); | ||
| 87 | Entry<?> translatedEntry = Objects.requireNonNull(deobfuscator.translate(reparentedEntry), "Translation failed"); | ||
| 88 | Collection<Entry<?>> siblings = obfToDeobf.getSiblings(reparentedEntry).stream() | ||
| 89 | .filter(it->!entry.equals(it))//e.g. for root classes, ensure we dont match the name against itself | ||
| 90 | .collect(Collectors.toList()); | ||
| 91 | for (Entry<?> childSibling : siblings) { | ||
| 92 | Entry<?> childSiblingTranslated = Objects.requireNonNull(deobfuscator.translate(childSibling), "Translation failed"); | ||
| 93 | if (translatedEntry.canConflictWith(childSiblingTranslated) && childSiblingTranslated.getName().equals(name) && !isAcceptableOverride(entry, childSibling)) { | ||
| 94 | Entry<?> parent = translatedEntry.getParent(); | ||
| 95 | if (parent != null) { | ||
| 96 | vc.raise(Message.NONUNIQUE_NAME_CLASS, name, parent); | ||
| 97 | } else { | ||
| 98 | vc.raise(Message.NONUNIQUE_NAME, name); | ||
| 99 | } | ||
| 100 | } | ||
| 101 | } | ||
| 102 | } | ||
| 103 | } | ||
| 104 | } | ||
| 66 | 105 | ||
| 67 | return relatedClasses; | 106 | private boolean isVisibleToChildren(Entry<?> entry) { |
| 107 | if (entry instanceof DefEntry) { | ||
| 108 | return !((DefEntry<?>) entry).getAccess().isPrivate(); | ||
| 109 | } | ||
| 110 | AccessFlags accessFlags = index.getEntryIndex().getEntryAccess(entry); | ||
| 111 | if (accessFlags != null) { | ||
| 112 | return !accessFlags.isPrivate(); | ||
| 113 | } | ||
| 114 | return true;//unknown, assume yes | ||
| 68 | } | 115 | } |
| 69 | 116 | ||
| 70 | private boolean isUnique(Entry<?> entry, Collection<Entry<?>> siblings, String name) { | 117 | private boolean isAcceptableOverride(Entry<?> ancestor, Entry<?> descendent) { |
| 71 | for (Entry<?> sibling : siblings) { | 118 | if (ancestor instanceof FieldEntry && descendent instanceof FieldEntry){ |
| 72 | if (entry.canConflictWith(sibling) && sibling.getName().equals(name)) { | 119 | return true;//fields don't apply here |
| 73 | return false; | 120 | } |
| 74 | } | 121 | |
| 122 | AccessFlags ancestorFlags = findAccessFlags(ancestor); | ||
| 123 | AccessFlags descendentFlags = findAccessFlags(descendent); | ||
| 124 | |||
| 125 | if (ancestorFlags == null || descendentFlags == null) { | ||
| 126 | return false;//we can't make any assumptions | ||
| 75 | } | 127 | } |
| 76 | return true; | 128 | |
| 129 | //bad == accessLevel < superAccessLevel | ||
| 130 | return !(descendentFlags.getAccessLevel() < ancestorFlags.getAccessLevel()); | ||
| 131 | } | ||
| 132 | |||
| 133 | private boolean isSynthetic(Entry<?> entry) { | ||
| 134 | AccessFlags accessFlags = findAccessFlags(entry); | ||
| 135 | return accessFlags != null && accessFlags.isSynthetic(); | ||
| 136 | } | ||
| 137 | |||
| 138 | private AccessFlags findAccessFlags(Entry<?> entry) { | ||
| 139 | return (entry instanceof DefEntry) ? ((DefEntry<?>) entry).getAccess() : index.getEntryIndex() | ||
| 140 | .getEntryAccess(entry); | ||
| 77 | } | 141 | } |
| 78 | 142 | ||
| 79 | } | 143 | } |
diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/AccessFlags.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/AccessFlags.java index b280eef2..aa48a5b2 100644 --- a/enigma/src/main/java/cuchaz/enigma/translation/representation/AccessFlags.java +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/AccessFlags.java | |||
| @@ -8,6 +8,10 @@ import java.lang.reflect.Modifier; | |||
| 8 | public class AccessFlags { | 8 | public class AccessFlags { |
| 9 | public static final AccessFlags PRIVATE = new AccessFlags(Opcodes.ACC_PRIVATE); | 9 | public static final AccessFlags PRIVATE = new AccessFlags(Opcodes.ACC_PRIVATE); |
| 10 | public static final AccessFlags PUBLIC = new AccessFlags(Opcodes.ACC_PUBLIC); | 10 | public static final AccessFlags PUBLIC = new AccessFlags(Opcodes.ACC_PUBLIC); |
| 11 | public static final int ACCESS_LEVEL_PUBLIC = 4; | ||
| 12 | public static final int ACCESS_LEVEL_PROTECTED = 3; | ||
| 13 | public static final int ACCESS_LEVEL_PACKAGE_LOCAL = 2; | ||
| 14 | public static final int ACCESS_LEVEL_PRIVATE = 1; | ||
| 11 | 15 | ||
| 12 | private int flags; | 16 | private int flags; |
| 13 | 17 | ||
| @@ -89,6 +93,23 @@ public class AccessFlags { | |||
| 89 | return this.flags; | 93 | return this.flags; |
| 90 | } | 94 | } |
| 91 | 95 | ||
| 96 | /** | ||
| 97 | * Adapted from https://github.com/JetBrains/intellij-community/blob/6472c347db91d11bbf02895a767198f9d884b119/java/java-psi-api/src/com/intellij/psi/util/PsiUtil.java#L389 | ||
| 98 | * @return visibility access level on a 'weakness scale' | ||
| 99 | */ | ||
| 100 | public int getAccessLevel() { | ||
| 101 | if (isPrivate()) { | ||
| 102 | return ACCESS_LEVEL_PRIVATE; | ||
| 103 | } | ||
| 104 | if (isProtected()) { | ||
| 105 | return ACCESS_LEVEL_PROTECTED; | ||
| 106 | } | ||
| 107 | if (isPublic()) { | ||
| 108 | return ACCESS_LEVEL_PUBLIC; | ||
| 109 | } | ||
| 110 | return ACCESS_LEVEL_PACKAGE_LOCAL; | ||
| 111 | } | ||
| 112 | |||
| 92 | @Override | 113 | @Override |
| 93 | public boolean equals(Object obj) { | 114 | public boolean equals(Object obj) { |
| 94 | return obj instanceof AccessFlags && ((AccessFlags) obj).flags == flags; | 115 | return obj instanceof AccessFlags && ((AccessFlags) obj).flags == flags; |
diff --git a/enigma/src/test/java/cuchaz/enigma/TestAllowableClashes.java b/enigma/src/test/java/cuchaz/enigma/TestAllowableClashes.java new file mode 100644 index 00000000..bfc21de3 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TestAllowableClashes.java | |||
| @@ -0,0 +1,52 @@ | |||
| 1 | package cuchaz.enigma; | ||
| 2 | |||
| 3 | import cuchaz.enigma.classprovider.ClasspathClassProvider; | ||
| 4 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 5 | import cuchaz.enigma.translation.mapping.EntryRemapper; | ||
| 6 | import cuchaz.enigma.translation.mapping.serde.MappingFormat; | ||
| 7 | import cuchaz.enigma.translation.mapping.serde.MappingParseException; | ||
| 8 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 9 | import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; | ||
| 10 | import cuchaz.enigma.translation.representation.entry.MethodEntry; | ||
| 11 | import cuchaz.enigma.utils.validation.ValidationContext; | ||
| 12 | import org.hamcrest.MatcherAssert; | ||
| 13 | import org.junit.Assert; | ||
| 14 | import org.junit.Test; | ||
| 15 | |||
| 16 | import java.io.IOException; | ||
| 17 | import java.nio.file.Paths; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Test that we can accept some name clashes that are allowed by javac | ||
| 21 | */ | ||
| 22 | public class TestAllowableClashes { | ||
| 23 | |||
| 24 | private static final String inputBaseName = "build/test-obf/visibility"; | ||
| 25 | |||
| 26 | @Test | ||
| 27 | public void test() throws IOException, MappingParseException { | ||
| 28 | //Load produced mappings | ||
| 29 | Enigma enigma = Enigma.create(); | ||
| 30 | EnigmaProject project = enigma.openJar(Paths.get(inputBaseName + ".jar"), new ClasspathClassProvider(), ProgressListener.none()); | ||
| 31 | EntryTree<EntryMapping> obfToDeobf = MappingFormat.PROGUARD.read(Paths.get(inputBaseName + "-mapping.txt"), ProgressListener.none(), null); | ||
| 32 | |||
| 33 | //Load them into enigma, none should conflict | ||
| 34 | EntryRemapper mapper = project.getMapper(); | ||
| 35 | for (int round=0; round<2; round++) { | ||
| 36 | for (EntryTreeNode<EntryMapping> node : obfToDeobf) { | ||
| 37 | Assert.assertNotEquals(null, node.getValue()); | ||
| 38 | if (node.getEntry() instanceof MethodEntry && (node.getEntry() | ||
| 39 | .getName() | ||
| 40 | .equals("<init>") || node.getEntry().getName().equals("<clinit>"))) { | ||
| 41 | //skip proguard's constructor entries | ||
| 42 | continue; | ||
| 43 | } | ||
| 44 | System.out.println(node.getEntry().toString() + " -> " + node.getValue().getTargetName()); | ||
| 45 | ValidationContext vc = new ValidationContext(); | ||
| 46 | mapper.mapFromObf(vc, node.getEntry(), node.getValue()); | ||
| 47 | MatcherAssert.assertThat(vc, ValidationContextMatcher.INSTANCE); | ||
| 48 | } | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | } | ||
diff --git a/enigma/src/test/java/cuchaz/enigma/ValidationContextMatcher.java b/enigma/src/test/java/cuchaz/enigma/ValidationContextMatcher.java new file mode 100644 index 00000000..5404077f --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/ValidationContextMatcher.java | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | package cuchaz.enigma; | ||
| 2 | |||
| 3 | import cuchaz.enigma.utils.validation.ParameterizedMessage; | ||
| 4 | import cuchaz.enigma.utils.validation.ValidationContext; | ||
| 5 | import org.hamcrest.CustomMatcher; | ||
| 6 | import org.hamcrest.Description; | ||
| 7 | |||
| 8 | class ValidationContextMatcher extends CustomMatcher<ValidationContext> { | ||
| 9 | public static final ValidationContextMatcher INSTANCE = new ValidationContextMatcher(); | ||
| 10 | |||
| 11 | private ValidationContextMatcher() { | ||
| 12 | super("ValidationContext can proceed"); | ||
| 13 | } | ||
| 14 | |||
| 15 | @Override | ||
| 16 | public boolean matches(Object item) { | ||
| 17 | return item instanceof ValidationContext && ((ValidationContext) item).canProceed(); | ||
| 18 | } | ||
| 19 | |||
| 20 | @Override | ||
| 21 | public void describeMismatch(Object item, Description description) { | ||
| 22 | if (!(item instanceof ValidationContext)) { | ||
| 23 | description.appendText("expected ValidationContext, was").appendValue(item); | ||
| 24 | return; | ||
| 25 | } | ||
| 26 | ValidationContext vc = (ValidationContext) item; | ||
| 27 | for (ParameterizedMessage message : vc.getMessages()) { | ||
| 28 | description.appendText(message.getText()); | ||
| 29 | String longMessage = message.getLongText(); | ||
| 30 | if (longMessage != null && !longMessage.trim().isEmpty()){ | ||
| 31 | description.appendText(longMessage); | ||
| 32 | } | ||
| 33 | } | ||
| 34 | } | ||
| 35 | } | ||
diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/visibility/ClassA.java b/enigma/src/test/java/cuchaz/enigma/inputs/visibility/ClassA.java new file mode 100644 index 00000000..458f7c0d --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/visibility/ClassA.java | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | package cuchaz.enigma.inputs.visibility; | ||
| 2 | |||
| 3 | class ClassA { | ||
| 4 | |||
| 5 | protected Object protectedParentPrivateChild; | ||
| 6 | public Object publicParentPrivateChild; | ||
| 7 | |||
| 8 | public static Object LOGGER = null; | ||
| 9 | |||
| 10 | protected static Object LOGGER2 = null; | ||
| 11 | |||
| 12 | public Object publicPublic; | ||
| 13 | |||
| 14 | public static void equalAccessStatic() {} | ||
| 15 | |||
| 16 | protected static void protectedPublicStatic(){} | ||
| 17 | |||
| 18 | private static void privateStaticParentPublicStaticChild(){} | ||
| 19 | |||
| 20 | private void privateParentPublicStaticChild() {} | ||
| 21 | |||
| 22 | static void packagePrivateParentProtectedChild(){} | ||
| 23 | |||
| 24 | private static void packagePrivateChild(){} | ||
| 25 | |||
| 26 | public ClassA returningSubclass(){return null;} | ||
| 27 | |||
| 28 | } \ No newline at end of file | ||
diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/visibility/ClassB.java b/enigma/src/test/java/cuchaz/enigma/inputs/visibility/ClassB.java new file mode 100644 index 00000000..2a1b68a7 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/visibility/ClassB.java | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | package cuchaz.enigma.inputs.visibility; | ||
| 2 | |||
| 3 | public class ClassB extends ClassA { | ||
| 4 | private Object protectedParentPrivateChild; | ||
| 5 | |||
| 6 | private Object publicParentPrivateChild; | ||
| 7 | |||
| 8 | public Object publicPublic; | ||
| 9 | |||
| 10 | public static Object LOGGER; | ||
| 11 | |||
| 12 | public static Object LOGGER2 = null; | ||
| 13 | |||
| 14 | public static void equalAccessStatic() { | ||
| 15 | } | ||
| 16 | |||
| 17 | public static void protectedPublicStatic() { | ||
| 18 | } | ||
| 19 | |||
| 20 | public static void privateStaticParentPublicStaticChild() { | ||
| 21 | } | ||
| 22 | |||
| 23 | public static void privateParentPublicStaticChild() { | ||
| 24 | } | ||
| 25 | |||
| 26 | protected static void packagePrivateParentProtectedChild(){} | ||
| 27 | |||
| 28 | static void packagePrivateChild(){} | ||
| 29 | |||
| 30 | public ClassB returningSubclass(){return null;} | ||
| 31 | } | ||