From 0f47403d0220757fed189b76e2071e25b1025cb8 Mon Sep 17 00:00:00 2001 From: Runemoro Date: Wed, 3 Jun 2020 13:39:42 -0400 Subject: Split GUI code to separate module (#242) * Split into modules * Post merge compile fixes Co-authored-by: modmuss50 --- .../src/main/java/cuchaz/enigma/ClassProvider.java | 10 + enigma/src/main/java/cuchaz/enigma/Enigma.java | 139 +++++++ .../src/main/java/cuchaz/enigma/EnigmaProfile.java | 131 +++++++ .../src/main/java/cuchaz/enigma/EnigmaProject.java | 288 ++++++++++++++ .../main/java/cuchaz/enigma/EnigmaServices.java | 20 + .../main/java/cuchaz/enigma/ProgressListener.java | 19 + .../main/java/cuchaz/enigma/analysis/Access.java | 43 ++ .../java/cuchaz/enigma/analysis/BuiltinPlugin.java | 156 ++++++++ .../java/cuchaz/enigma/analysis/ClassCache.java | 126 ++++++ .../analysis/ClassImplementationsTreeNode.java | 72 ++++ .../enigma/analysis/ClassInheritanceTreeNode.java | 72 ++++ .../enigma/analysis/ClassReferenceTreeNode.java | 94 +++++ .../cuchaz/enigma/analysis/EntryReference.java | 140 +++++++ .../enigma/analysis/FieldReferenceTreeNode.java | 83 ++++ .../enigma/analysis/IndexSimpleVerifier.java | 154 ++++++++ .../cuchaz/enigma/analysis/IndexTreeBuilder.java | 74 ++++ .../cuchaz/enigma/analysis/InterpreterPair.java | 130 +++++++ .../analysis/MethodImplementationsTreeNode.java | 85 ++++ .../enigma/analysis/MethodInheritanceTreeNode.java | 95 +++++ .../enigma/analysis/MethodNodeWithAction.java | 19 + .../enigma/analysis/MethodReferenceTreeNode.java | 113 ++++++ .../enigma/analysis/ReferenceTargetType.java | 74 ++++ .../cuchaz/enigma/analysis/ReferenceTreeNode.java | 20 + .../enigma/analysis/index/BridgeMethodIndex.java | 156 ++++++++ .../cuchaz/enigma/analysis/index/EntryIndex.java | 102 +++++ .../enigma/analysis/index/IndexClassVisitor.java | 40 ++ .../analysis/index/IndexReferenceVisitor.java | 180 +++++++++ .../enigma/analysis/index/InheritanceIndex.java | 127 ++++++ .../cuchaz/enigma/analysis/index/JarIndex.java | 170 ++++++++ .../cuchaz/enigma/analysis/index/JarIndexer.java | 28 ++ .../analysis/index/PackageVisibilityIndex.java | 147 +++++++ .../enigma/analysis/index/ReferenceIndex.java | 148 +++++++ .../main/java/cuchaz/enigma/api/EnigmaPlugin.java | 5 + .../cuchaz/enigma/api/EnigmaPluginContext.java | 9 + .../cuchaz/enigma/api/service/EnigmaService.java | 4 + .../enigma/api/service/EnigmaServiceContext.java | 11 + .../enigma/api/service/EnigmaServiceFactory.java | 5 + .../enigma/api/service/EnigmaServiceType.java | 29 ++ .../enigma/api/service/JarIndexerService.java | 10 + .../enigma/api/service/NameProposalService.java | 12 + .../enigma/api/service/ObfuscationTestService.java | 9 + .../bytecode/translators/AsmObjectTranslator.java | 46 +++ .../translators/LocalVariableFixVisitor.java | 126 ++++++ .../bytecode/translators/SourceFixVisitor.java | 39 ++ .../translators/TranslationAnnotationVisitor.java | 51 +++ .../translators/TranslationClassVisitor.java | 102 +++++ .../translators/TranslationFieldVisitor.java | 33 ++ .../translators/TranslationMethodVisitor.java | 145 +++++++ .../translators/TranslationSignatureVisitor.java | 129 ++++++ .../main/java/cuchaz/enigma/source/Decompiler.java | 5 + .../cuchaz/enigma/source/DecompilerService.java | 11 + .../java/cuchaz/enigma/source/Decompilers.java | 9 + .../src/main/java/cuchaz/enigma/source/Source.java | 11 + .../java/cuchaz/enigma/source/SourceIndex.java | 172 ++++++++ .../java/cuchaz/enigma/source/SourceRemapper.java | 62 +++ .../java/cuchaz/enigma/source/SourceSettings.java | 11 + .../src/main/java/cuchaz/enigma/source/Token.java | 72 ++++ .../cuchaz/enigma/source/cfr/CfrDecompiler.java | 108 +++++ .../java/cuchaz/enigma/source/cfr/CfrSource.java | 38 ++ .../cuchaz/enigma/source/cfr/EnigmaDumper.java | 433 +++++++++++++++++++++ .../cuchaz/enigma/source/procyon/EntryParser.java | 49 +++ .../enigma/source/procyon/ProcyonDecompiler.java | 84 ++++ .../enigma/source/procyon/ProcyonSource.java | 49 +++ .../procyon/index/SourceIndexClassVisitor.java | 95 +++++ .../procyon/index/SourceIndexMethodVisitor.java | 218 +++++++++++ .../source/procyon/index/SourceIndexVisitor.java | 40 ++ .../enigma/source/procyon/index/TokenFactory.java | 46 +++ .../transformers/AddJavadocsAstTransform.java | 134 +++++++ .../transformers/DropImportAstTransform.java | 33 ++ .../transformers/DropVarModifiersAstTransform.java | 37 ++ .../procyon/transformers/InvalidIdentifierFix.java | 29 ++ .../source/procyon/transformers/Java8Generics.java | 107 +++++ .../ObfuscatedEnumSwitchRewriterTransform.java | 414 ++++++++++++++++++++ .../procyon/transformers/RemoveObjectCasts.java | 39 ++ .../source/procyon/transformers/VarargsFixer.java | 197 ++++++++++ .../typeloader/CachingClasspathTypeLoader.java | 33 ++ .../procyon/typeloader/CachingTypeLoader.java | 38 ++ .../typeloader/CompiledSourceTypeLoader.java | 140 +++++++ .../procyon/typeloader/NoRetryMetadataSystem.java | 38 ++ .../procyon/typeloader/SynchronizedTypeLoader.java | 20 + .../enigma/translation/LocalNameGenerator.java | 44 +++ .../enigma/translation/MappingTranslator.java | 24 ++ .../enigma/translation/ProposingTranslator.java | 51 +++ .../enigma/translation/SignatureUpdater.java | 92 +++++ .../cuchaz/enigma/translation/Translatable.java | 9 + .../enigma/translation/TranslationDirection.java | 36 ++ .../java/cuchaz/enigma/translation/Translator.java | 61 +++ .../cuchaz/enigma/translation/VoidTranslator.java | 10 + .../enigma/translation/mapping/AccessModifier.java | 25 ++ .../enigma/translation/mapping/EntryMap.java | 24 ++ .../enigma/translation/mapping/EntryMapping.java | 75 ++++ .../enigma/translation/mapping/EntryRemapper.java | 104 +++++ .../enigma/translation/mapping/EntryResolver.java | 41 ++ .../translation/mapping/IllegalNameException.java | 39 ++ .../translation/mapping/IndexEntryResolver.java | 227 +++++++++++ .../enigma/translation/mapping/MappingDelta.java | 54 +++ .../translation/mapping/MappingOperations.java | 71 ++++ .../enigma/translation/mapping/MappingPair.java | 32 ++ .../translation/mapping/MappingValidator.java | 75 ++++ .../translation/mapping/MappingsChecker.java | 99 +++++ .../enigma/translation/mapping/NameValidator.java | 50 +++ .../translation/mapping/ResolutionStrategy.java | 6 + .../translation/mapping/VoidEntryResolver.java | 27 ++ .../translation/mapping/serde/LfPrintWriter.java | 16 + .../mapping/serde/MappingFileNameFormat.java | 10 + .../translation/mapping/serde/MappingFormat.java | 65 ++++ .../translation/mapping/serde/MappingHelper.java | 51 +++ .../mapping/serde/MappingParseException.java | 39 ++ .../mapping/serde/MappingSaveParameters.java | 16 + .../translation/mapping/serde/MappingsReader.java | 12 + .../translation/mapping/serde/MappingsWriter.java | 16 + .../translation/mapping/serde/RawEntryMapping.java | 30 ++ .../mapping/serde/enigma/EnigmaFormat.java | 9 + .../mapping/serde/enigma/EnigmaMappingsReader.java | 322 +++++++++++++++ .../mapping/serde/enigma/EnigmaMappingsWriter.java | 318 +++++++++++++++ .../serde/proguard/ProguardMappingsReader.java | 135 +++++++ .../mapping/serde/srg/SrgMappingsWriter.java | 119 ++++++ .../mapping/serde/tiny/TinyMappingsReader.java | 116 ++++++ .../mapping/serde/tiny/TinyMappingsWriter.java | 149 +++++++ .../mapping/serde/tinyv2/TinyV2Reader.java | 296 ++++++++++++++ .../mapping/serde/tinyv2/TinyV2Writer.java | 172 ++++++++ .../mapping/tree/DeltaTrackingTree.java | 110 ++++++ .../enigma/translation/mapping/tree/EntryTree.java | 26 ++ .../translation/mapping/tree/EntryTreeNode.java | 40 ++ .../translation/mapping/tree/HashEntryTree.java | 188 +++++++++ .../translation/mapping/tree/HashTreeNode.java | 75 ++++ .../translation/representation/AccessFlags.java | 116 ++++++ .../enigma/translation/representation/Lambda.java | 105 +++++ .../representation/MethodDescriptor.java | 132 +++++++ .../translation/representation/Signature.java | 98 +++++ .../translation/representation/TypeDescriptor.java | 268 +++++++++++++ .../representation/entry/ClassDefEntry.java | 93 +++++ .../representation/entry/ClassEntry.java | 214 ++++++++++ .../translation/representation/entry/DefEntry.java | 7 + .../translation/representation/entry/Entry.java | 107 +++++ .../representation/entry/FieldDefEntry.java | 71 ++++ .../representation/entry/FieldEntry.java | 96 +++++ .../entry/LocalVariableDefEntry.java | 51 +++ .../representation/entry/LocalVariableEntry.java | 93 +++++ .../representation/entry/MethodDefEntry.java | 71 ++++ .../representation/entry/MethodEntry.java | 105 +++++ .../representation/entry/ParentedEntry.java | 81 ++++ enigma/src/main/java/cuchaz/enigma/utils/I18n.java | 95 +++++ enigma/src/main/java/cuchaz/enigma/utils/Pair.java | 26 ++ .../src/main/java/cuchaz/enigma/utils/Utils.java | 92 +++++ .../services/cuchaz.enigma.api.EnigmaPlugin | 1 + enigma/src/main/resources/lang/en_us.json | 164 ++++++++ enigma/src/main/resources/lang/fr_fr.json | 164 ++++++++ enigma/src/main/resources/lang/zh_cn.json | 118 ++++++ enigma/src/main/resources/profile.json | 20 + .../cuchaz/enigma/PackageVisibilityIndexTest.java | 53 +++ .../src/test/java/cuchaz/enigma/TestDeobfed.java | 106 +++++ .../test/java/cuchaz/enigma/TestDeobfuscator.java | 41 ++ .../test/java/cuchaz/enigma/TestEntryFactory.java | 49 +++ .../test/java/cuchaz/enigma/TestInnerClasses.java | 85 ++++ .../enigma/TestJarIndexConstructorReferences.java | 124 ++++++ .../cuchaz/enigma/TestJarIndexInheritanceTree.java | 227 +++++++++++ .../java/cuchaz/enigma/TestJarIndexLoneClass.java | 157 ++++++++ .../java/cuchaz/enigma/TestMethodDescriptor.java | 247 ++++++++++++ .../java/cuchaz/enigma/TestTokensConstructors.java | 137 +++++++ .../test/java/cuchaz/enigma/TestTranslator.java | 155 ++++++++ .../java/cuchaz/enigma/TestTypeDescriptor.java | 243 ++++++++++++ .../src/test/java/cuchaz/enigma/TokenChecker.java | 65 ++++ .../src/test/java/cuchaz/enigma/inputs/Keep.java | 18 + .../enigma/inputs/constructors/BaseClass.java | 26 ++ .../cuchaz/enigma/inputs/constructors/Caller.java | 58 +++ .../inputs/constructors/DefaultConstructable.java | 16 + .../enigma/inputs/constructors/SubClass.java | 39 ++ .../enigma/inputs/constructors/SubSubClass.java | 22 ++ .../enigma/inputs/inheritanceTree/BaseClass.java | 32 ++ .../enigma/inputs/inheritanceTree/SubclassA.java | 22 ++ .../enigma/inputs/inheritanceTree/SubclassB.java | 41 ++ .../inputs/inheritanceTree/SubsubclassAA.java | 35 ++ .../enigma/inputs/innerClasses/A_Anonymous.java | 25 ++ .../innerClasses/B_AnonymousWithScopeArgs.java | 24 ++ .../inputs/innerClasses/C_ConstructorArgs.java | 31 ++ .../enigma/inputs/innerClasses/D_Simple.java | 19 + .../innerClasses/E_AnonymousWithOuterAccess.java | 32 ++ .../enigma/inputs/innerClasses/F_ClassTree.java | 30 ++ .../cuchaz/enigma/inputs/loneClass/LoneClass.java | 25 ++ .../cuchaz/enigma/inputs/packageAccess/Base.java | 7 + .../inputs/packageAccess/SamePackageChild.java | 12 + .../packageAccess/sub/OtherPackageChild.java | 14 + .../cuchaz/enigma/inputs/translation/A_Basic.java | 33 ++ .../enigma/inputs/translation/B_BaseClass.java | 26 ++ .../enigma/inputs/translation/C_SubClass.java | 28 ++ .../inputs/translation/D_AnonymousTesting.java | 29 ++ .../enigma/inputs/translation/E_Bridges.java | 32 ++ .../enigma/inputs/translation/F_ObjectMethods.java | 31 ++ .../enigma/inputs/translation/G_OuterClass.java | 36 ++ .../enigma/inputs/translation/H_NamelessClass.java | 40 ++ .../enigma/inputs/translation/I_Generics.java | 35 ++ .../enigma/translation/mapping/TestComments.java | 39 ++ .../mapping/TestTinyV2InnerClasses.java | 37 ++ .../enigma/translation/mapping/TestV2Main.java | 23 ++ enigma/src/test/resources/comments/test.mapping | 18 + enigma/src/test/resources/proguard-build.conf | 6 + enigma/src/test/resources/proguard-test.conf | 8 + .../test/resources/tinyV2InnerClasses/c.mapping | 2 + .../tinyV2InnerClasses/cuchaz/enigma/Dad.mapping | 5 + enigma/src/test/resources/translation.mappings | 41 ++ 201 files changed, 15472 insertions(+) create mode 100644 enigma/src/main/java/cuchaz/enigma/ClassProvider.java create mode 100644 enigma/src/main/java/cuchaz/enigma/Enigma.java create mode 100644 enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java create mode 100644 enigma/src/main/java/cuchaz/enigma/EnigmaProject.java create mode 100644 enigma/src/main/java/cuchaz/enigma/EnigmaServices.java create mode 100644 enigma/src/main/java/cuchaz/enigma/ProgressListener.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/Access.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/ClassCache.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/ClassImplementationsTreeNode.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/ClassInheritanceTreeNode.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/ClassReferenceTreeNode.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/FieldReferenceTreeNode.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/IndexSimpleVerifier.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/IndexTreeBuilder.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/InterpreterPair.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/MethodImplementationsTreeNode.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/MethodInheritanceTreeNode.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/MethodNodeWithAction.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/MethodReferenceTreeNode.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/ReferenceTargetType.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/ReferenceTreeNode.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/index/BridgeMethodIndex.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/index/EntryIndex.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/index/IndexClassVisitor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/index/IndexReferenceVisitor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/index/InheritanceIndex.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/index/JarIndex.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/index/JarIndexer.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/index/PackageVisibilityIndex.java create mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/index/ReferenceIndex.java create mode 100644 enigma/src/main/java/cuchaz/enigma/api/EnigmaPlugin.java create mode 100644 enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java create mode 100644 enigma/src/main/java/cuchaz/enigma/api/service/EnigmaService.java create mode 100644 enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java create mode 100644 enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java create mode 100644 enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceType.java create mode 100644 enigma/src/main/java/cuchaz/enigma/api/service/JarIndexerService.java create mode 100644 enigma/src/main/java/cuchaz/enigma/api/service/NameProposalService.java create mode 100644 enigma/src/main/java/cuchaz/enigma/api/service/ObfuscationTestService.java create mode 100644 enigma/src/main/java/cuchaz/enigma/bytecode/translators/AsmObjectTranslator.java create mode 100644 enigma/src/main/java/cuchaz/enigma/bytecode/translators/LocalVariableFixVisitor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/bytecode/translators/SourceFixVisitor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationAnnotationVisitor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationClassVisitor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationFieldVisitor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationMethodVisitor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationSignatureVisitor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/Decompiler.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/DecompilerService.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/Decompilers.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/Source.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/SourceIndex.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/SourceRemapper.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/SourceSettings.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/Token.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDecompiler.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/cfr/CfrSource.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/cfr/EnigmaDumper.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/EntryParser.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/ProcyonDecompiler.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/ProcyonSource.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/index/SourceIndexClassVisitor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/index/SourceIndexMethodVisitor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/index/SourceIndexVisitor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/index/TokenFactory.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/AddJavadocsAstTransform.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/DropImportAstTransform.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/DropVarModifiersAstTransform.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/InvalidIdentifierFix.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/Java8Generics.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/ObfuscatedEnumSwitchRewriterTransform.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/RemoveObjectCasts.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/VarargsFixer.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingClasspathTypeLoader.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingTypeLoader.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CompiledSourceTypeLoader.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/NoRetryMetadataSystem.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/SynchronizedTypeLoader.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/MappingTranslator.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/ProposingTranslator.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/SignatureUpdater.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/Translatable.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/TranslationDirection.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/Translator.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/VoidTranslator.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/AccessModifier.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryMap.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryMapping.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryResolver.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/IndexEntryResolver.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingDelta.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingOperations.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingPair.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingsChecker.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/ResolutionStrategy.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/VoidEntryResolver.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/LfPrintWriter.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFileNameFormat.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFormat.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingHelper.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingParseException.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingSaveParameters.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsReader.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsWriter.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/RawEntryMapping.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/enigma/EnigmaFormat.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/enigma/EnigmaMappingsReader.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/enigma/EnigmaMappingsWriter.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/proguard/ProguardMappingsReader.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/srg/SrgMappingsWriter.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tiny/TinyMappingsReader.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tiny/TinyMappingsWriter.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tinyv2/TinyV2Reader.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tinyv2/TinyV2Writer.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/DeltaTrackingTree.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTree.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTreeNode.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/HashEntryTree.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/HashTreeNode.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/AccessFlags.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/Lambda.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/MethodDescriptor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/Signature.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/TypeDescriptor.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassDefEntry.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/entry/DefEntry.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/entry/FieldDefEntry.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/entry/FieldEntry.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableDefEntry.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableEntry.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/entry/MethodDefEntry.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/entry/MethodEntry.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ParentedEntry.java create mode 100644 enigma/src/main/java/cuchaz/enigma/utils/I18n.java create mode 100644 enigma/src/main/java/cuchaz/enigma/utils/Pair.java create mode 100644 enigma/src/main/java/cuchaz/enigma/utils/Utils.java create mode 100644 enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin create mode 100644 enigma/src/main/resources/lang/en_us.json create mode 100644 enigma/src/main/resources/lang/fr_fr.json create mode 100644 enigma/src/main/resources/lang/zh_cn.json create mode 100644 enigma/src/main/resources/profile.json create mode 100644 enigma/src/test/java/cuchaz/enigma/PackageVisibilityIndexTest.java create mode 100644 enigma/src/test/java/cuchaz/enigma/TestDeobfed.java create mode 100644 enigma/src/test/java/cuchaz/enigma/TestDeobfuscator.java create mode 100644 enigma/src/test/java/cuchaz/enigma/TestEntryFactory.java create mode 100644 enigma/src/test/java/cuchaz/enigma/TestInnerClasses.java create mode 100644 enigma/src/test/java/cuchaz/enigma/TestJarIndexConstructorReferences.java create mode 100644 enigma/src/test/java/cuchaz/enigma/TestJarIndexInheritanceTree.java create mode 100644 enigma/src/test/java/cuchaz/enigma/TestJarIndexLoneClass.java create mode 100644 enigma/src/test/java/cuchaz/enigma/TestMethodDescriptor.java create mode 100644 enigma/src/test/java/cuchaz/enigma/TestTokensConstructors.java create mode 100644 enigma/src/test/java/cuchaz/enigma/TestTranslator.java create mode 100644 enigma/src/test/java/cuchaz/enigma/TestTypeDescriptor.java create mode 100644 enigma/src/test/java/cuchaz/enigma/TokenChecker.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/Keep.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/constructors/BaseClass.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/constructors/Caller.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/constructors/DefaultConstructable.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/constructors/SubClass.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/constructors/SubSubClass.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/BaseClass.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/SubclassA.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/SubclassB.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/SubsubclassAA.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/A_Anonymous.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/B_AnonymousWithScopeArgs.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/C_ConstructorArgs.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/D_Simple.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/E_AnonymousWithOuterAccess.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/F_ClassTree.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/loneClass/LoneClass.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/packageAccess/Base.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/packageAccess/SamePackageChild.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/packageAccess/sub/OtherPackageChild.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/translation/A_Basic.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/translation/B_BaseClass.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/translation/C_SubClass.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/translation/D_AnonymousTesting.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/translation/E_Bridges.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/translation/F_ObjectMethods.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/translation/G_OuterClass.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/translation/H_NamelessClass.java create mode 100644 enigma/src/test/java/cuchaz/enigma/inputs/translation/I_Generics.java create mode 100644 enigma/src/test/java/cuchaz/enigma/translation/mapping/TestComments.java create mode 100644 enigma/src/test/java/cuchaz/enigma/translation/mapping/TestTinyV2InnerClasses.java create mode 100644 enigma/src/test/java/cuchaz/enigma/translation/mapping/TestV2Main.java create mode 100644 enigma/src/test/resources/comments/test.mapping create mode 100644 enigma/src/test/resources/proguard-build.conf create mode 100644 enigma/src/test/resources/proguard-test.conf create mode 100644 enigma/src/test/resources/tinyV2InnerClasses/c.mapping create mode 100644 enigma/src/test/resources/tinyV2InnerClasses/cuchaz/enigma/Dad.mapping create mode 100644 enigma/src/test/resources/translation.mappings (limited to 'enigma/src') diff --git a/enigma/src/main/java/cuchaz/enigma/ClassProvider.java b/enigma/src/main/java/cuchaz/enigma/ClassProvider.java new file mode 100644 index 0000000..2b91379 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/ClassProvider.java @@ -0,0 +1,10 @@ +package cuchaz.enigma; + +import org.objectweb.asm.tree.ClassNode; + +import javax.annotation.Nullable; + +public interface ClassProvider { + @Nullable + ClassNode getClassNode(String name); +} diff --git a/enigma/src/main/java/cuchaz/enigma/Enigma.java b/enigma/src/main/java/cuchaz/enigma/Enigma.java new file mode 100644 index 0000000..73c9a09 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/Enigma.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableListMultimap; +import cuchaz.enigma.analysis.ClassCache; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.api.EnigmaPlugin; +import cuchaz.enigma.api.EnigmaPluginContext; +import cuchaz.enigma.api.service.EnigmaService; +import cuchaz.enigma.api.service.EnigmaServiceFactory; +import cuchaz.enigma.api.service.EnigmaServiceType; +import cuchaz.enigma.api.service.JarIndexerService; +import cuchaz.enigma.utils.Utils; +import org.objectweb.asm.Opcodes; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.ServiceLoader; + +public class Enigma { + public static final String NAME = "Enigma"; + public static final String VERSION; + public static final String URL = "https://fabricmc.net"; + public static final int ASM_VERSION = Opcodes.ASM8; + + private final EnigmaProfile profile; + private final EnigmaServices services; + + private Enigma(EnigmaProfile profile, EnigmaServices services) { + this.profile = profile; + this.services = services; + } + + public static Enigma create() { + return new Builder().build(); + } + + public static Builder builder() { + return new Builder(); + } + + public EnigmaProject openJar(Path path, ProgressListener progress) throws IOException { + ClassCache classCache = ClassCache.of(path); + JarIndex jarIndex = classCache.index(progress); + + services.get(JarIndexerService.TYPE).forEach(indexer -> indexer.acceptJar(classCache, jarIndex)); + + return new EnigmaProject(this, classCache, jarIndex, Utils.zipSha1(path)); + } + + public EnigmaProfile getProfile() { + return profile; + } + + public EnigmaServices getServices() { + return services; + } + + public static class Builder { + private EnigmaProfile profile = EnigmaProfile.EMPTY; + private Iterable plugins = ServiceLoader.load(EnigmaPlugin.class); + + private Builder() { + } + + public Builder setProfile(EnigmaProfile profile) { + Preconditions.checkNotNull(profile, "profile cannot be null"); + this.profile = profile; + return this; + } + + public Builder setPlugins(Iterable plugins) { + Preconditions.checkNotNull(plugins, "plugins cannot be null"); + this.plugins = plugins; + return this; + } + + public Enigma build() { + PluginContext pluginContext = new PluginContext(profile); + for (EnigmaPlugin plugin : plugins) { + plugin.init(pluginContext); + } + + EnigmaServices services = pluginContext.buildServices(); + return new Enigma(profile, services); + } + } + + private static class PluginContext implements EnigmaPluginContext { + private final EnigmaProfile profile; + + private final ImmutableListMultimap.Builder, EnigmaService> services = ImmutableListMultimap.builder(); + + PluginContext(EnigmaProfile profile) { + this.profile = profile; + } + + @Override + public void registerService(String id, EnigmaServiceType serviceType, EnigmaServiceFactory factory) { + List serviceProfiles = profile.getServiceProfiles(serviceType); + + for (EnigmaProfile.Service serviceProfile : serviceProfiles) { + if (serviceProfile.matches(id)) { + T service = factory.create(serviceProfile::getArgument); + services.put(serviceType, service); + break; + } + } + } + + EnigmaServices buildServices() { + return new EnigmaServices(services.build()); + } + } + + static { + String version = null; + + try { + version = Utils.readResourceToString("/version.txt"); + } catch (Throwable t) { + version = "Unknown Version"; + } + + VERSION = version; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java b/enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java new file mode 100644 index 0000000..daf2727 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java @@ -0,0 +1,131 @@ +package cuchaz.enigma; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import cuchaz.enigma.api.service.EnigmaServiceType; +import cuchaz.enigma.translation.mapping.serde.MappingFileNameFormat; +import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; + +import javax.annotation.Nullable; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public final class EnigmaProfile { + public static final EnigmaProfile EMPTY = new EnigmaProfile(new ServiceContainer(ImmutableMap.of())); + + private static final MappingSaveParameters DEFAULT_MAPPING_SAVE_PARAMETERS = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF); + private static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(ServiceContainer.class, (JsonDeserializer) EnigmaProfile::loadServiceContainer) + .create(); + private static final Type SERVICE_LIST_TYPE = new TypeToken>() { + }.getType(); + + @SerializedName("services") + private final ServiceContainer serviceProfiles; + + @SerializedName("mapping_save_parameters") + private final MappingSaveParameters mappingSaveParameters = null; + + private EnigmaProfile(ServiceContainer serviceProfiles) { + this.serviceProfiles = serviceProfiles; + } + + public static EnigmaProfile read(@Nullable Path file) throws IOException { + if (file != null) { + try (BufferedReader reader = Files.newBufferedReader(file)) { + return EnigmaProfile.parse(reader); + } + } else { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(EnigmaProfile.class.getResourceAsStream("/profile.json"), StandardCharsets.UTF_8))) { + return EnigmaProfile.parse(reader); + } catch (IOException ex) { + System.err.println("Failed to load default profile, will use empty profile: " + ex.getMessage()); + return EnigmaProfile.EMPTY; + } + } + } + + public static EnigmaProfile parse(Reader reader) { + return GSON.fromJson(reader, EnigmaProfile.class); + } + + private static ServiceContainer loadServiceContainer(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (!json.isJsonObject()) { + throw new JsonParseException("services must be an Object!"); + } + + JsonObject object = json.getAsJsonObject(); + + ImmutableMap.Builder> builder = ImmutableMap.builder(); + + for (Map.Entry entry : object.entrySet()) { + JsonElement value = entry.getValue(); + if (value.isJsonObject()) { + builder.put(entry.getKey(), Collections.singletonList(GSON.fromJson(value, Service.class))); + } else if (value.isJsonArray()) { + builder.put(entry.getKey(), GSON.fromJson(value, SERVICE_LIST_TYPE)); + } else { + throw new JsonParseException(String.format("Don't know how to convert %s to a list of service!", value)); + } + } + + return new ServiceContainer(builder.build()); + } + + public List getServiceProfiles(EnigmaServiceType serviceType) { + return serviceProfiles.get(serviceType.key); + } + + public MappingSaveParameters getMappingSaveParameters() { + //noinspection ConstantConditions + return mappingSaveParameters == null ? EnigmaProfile.DEFAULT_MAPPING_SAVE_PARAMETERS : mappingSaveParameters; + } + + public static class Service { + private final String id; + private final Map args; + + Service(String id, Map args) { + this.id = id; + this.args = args; + } + + public boolean matches(String id) { + return this.id.equals(id); + } + + public Optional getArgument(String key) { + return args != null ? Optional.ofNullable(args.get(key)) : Optional.empty(); + } + } + + static final class ServiceContainer { + private final Map> services; + + ServiceContainer(Map> services) { + this.services = services; + } + + List get(String key) { + return services.getOrDefault(key, Collections.emptyList()); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java b/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java new file mode 100644 index 0000000..43ea14e --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java @@ -0,0 +1,288 @@ +package cuchaz.enigma; + +import com.google.common.base.Functions; +import com.google.common.base.Preconditions; +import cuchaz.enigma.analysis.ClassCache; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.api.service.NameProposalService; +import cuchaz.enigma.bytecode.translators.SourceFixVisitor; +import cuchaz.enigma.bytecode.translators.TranslationClassVisitor; +import cuchaz.enigma.source.*; +import cuchaz.enigma.translation.ProposingTranslator; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.*; +import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import cuchaz.enigma.utils.I18n; + +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.ClassNode; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.stream.Collectors; + +public class EnigmaProject { + private final Enigma enigma; + + private final ClassCache classCache; + private final JarIndex jarIndex; + private final byte[] jarChecksum; + + private EntryRemapper mapper; + + public EnigmaProject(Enigma enigma, ClassCache classCache, JarIndex jarIndex, byte[] jarChecksum) { + Preconditions.checkArgument(jarChecksum.length == 20); + this.enigma = enigma; + this.classCache = classCache; + this.jarIndex = jarIndex; + this.jarChecksum = jarChecksum; + + this.mapper = EntryRemapper.empty(jarIndex); + } + + public void setMappings(EntryTree mappings) { + if (mappings != null) { + mapper = EntryRemapper.mapped(jarIndex, mappings); + } else { + mapper = EntryRemapper.empty(jarIndex); + } + } + + public Enigma getEnigma() { + return enigma; + } + + public ClassCache getClassCache() { + return classCache; + } + + public JarIndex getJarIndex() { + return jarIndex; + } + + public byte[] getJarChecksum() { + return jarChecksum; + } + + public EntryRemapper getMapper() { + return mapper; + } + + public void dropMappings(ProgressListener progress) { + DeltaTrackingTree mappings = mapper.getObfToDeobf(); + + Collection> dropped = dropMappings(mappings, progress); + for (Entry entry : dropped) { + mappings.trackChange(entry); + } + } + + private Collection> dropMappings(EntryTree mappings, ProgressListener progress) { + // drop mappings that don't match the jar + MappingsChecker checker = new MappingsChecker(jarIndex, mappings); + MappingsChecker.Dropped dropped = checker.dropBrokenMappings(progress); + + Map, String> droppedMappings = dropped.getDroppedMappings(); + for (Map.Entry, String> mapping : droppedMappings.entrySet()) { + System.out.println("WARNING: Couldn't find " + mapping.getKey() + " (" + mapping.getValue() + ") in jar. Mapping was dropped."); + } + + return droppedMappings.keySet(); + } + + public Decompiler createDecompiler(DecompilerService decompilerService) { + return decompilerService.create(name -> { + ClassNode node = this.getClassCache().getClassNode(name); + + if (node == null) { + return null; + } + + ClassNode fixedNode = new ClassNode(); + node.accept(new SourceFixVisitor(Enigma.ASM_VERSION, fixedNode, getJarIndex())); + return fixedNode; + }, new SourceSettings(true, true)); + } + + public boolean isRenamable(Entry obfEntry) { + if (obfEntry instanceof MethodEntry) { + // HACKHACK: Object methods are not obfuscated identifiers + MethodEntry obfMethodEntry = (MethodEntry) obfEntry; + String name = obfMethodEntry.getName(); + String sig = obfMethodEntry.getDesc().toString(); + if (name.equals("clone") && sig.equals("()Ljava/lang/Object;")) { + return false; + } else if (name.equals("equals") && sig.equals("(Ljava/lang/Object;)Z")) { + return false; + } else if (name.equals("finalize") && sig.equals("()V")) { + return false; + } else if (name.equals("getClass") && sig.equals("()Ljava/lang/Class;")) { + return false; + } else if (name.equals("hashCode") && sig.equals("()I")) { + return false; + } else if (name.equals("notify") && sig.equals("()V")) { + return false; + } else if (name.equals("notifyAll") && sig.equals("()V")) { + return false; + } else if (name.equals("toString") && sig.equals("()Ljava/lang/String;")) { + return false; + } else if (name.equals("wait") && sig.equals("()V")) { + return false; + } else if (name.equals("wait") && sig.equals("(J)V")) { + return false; + } else if (name.equals("wait") && sig.equals("(JI)V")) { + return false; + } + } else if (obfEntry instanceof LocalVariableEntry && !((LocalVariableEntry) obfEntry).isArgument()) { + return false; + } + + return this.jarIndex.getEntryIndex().hasEntry(obfEntry); + } + + public boolean isRenamable(EntryReference, Entry> obfReference) { + return obfReference.isNamed() && isRenamable(obfReference.getNameableEntry()); + } + + public JarExport exportRemappedJar(ProgressListener progress) { + Collection classEntries = jarIndex.getEntryIndex().getClasses(); + + NameProposalService[] nameProposalServices = getEnigma().getServices().get(NameProposalService.TYPE).toArray(new NameProposalService[0]); + Translator deobfuscator = nameProposalServices.length == 0 ? mapper.getDeobfuscator() : new ProposingTranslator(mapper, nameProposalServices); + + AtomicInteger count = new AtomicInteger(); + progress.init(classEntries.size(), I18n.translate("progress.classes.deobfuscating")); + + Map compiled = classEntries.parallelStream() + .map(entry -> { + ClassEntry translatedEntry = deobfuscator.translate(entry); + progress.step(count.getAndIncrement(), translatedEntry.toString()); + + ClassNode node = classCache.getClassNode(entry.getFullName()); + if (node != null) { + ClassNode translatedNode = new ClassNode(); + node.accept(new TranslationClassVisitor(deobfuscator, Enigma.ASM_VERSION, new SourceFixVisitor(Enigma.ASM_VERSION, translatedNode, jarIndex))); + return translatedNode; + } + + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toMap(n -> n.name, Functions.identity())); + + return new JarExport(jarIndex, compiled); + } + + public static final class JarExport { + private final JarIndex jarIndex; + private final Map compiled; + + JarExport(JarIndex jarIndex, Map compiled) { + this.jarIndex = jarIndex; + this.compiled = compiled; + } + + public void write(Path path, ProgressListener progress) throws IOException { + progress.init(this.compiled.size(), I18n.translate("progress.jar.writing")); + + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(path))) { + AtomicInteger count = new AtomicInteger(); + + for (ClassNode node : this.compiled.values()) { + progress.step(count.getAndIncrement(), node.name); + + String entryName = node.name.replace('.', '/') + ".class"; + + ClassWriter writer = new ClassWriter(0); + node.accept(writer); + + out.putNextEntry(new JarEntry(entryName)); + out.write(writer.toByteArray()); + out.closeEntry(); + } + } + } + + public SourceExport decompile(ProgressListener progress, DecompilerService decompilerService) { + Collection classes = this.compiled.values().stream() + .filter(classNode -> classNode.name.indexOf('$') == -1) + .collect(Collectors.toList()); + + progress.init(classes.size(), I18n.translate("progress.classes.decompiling")); + + //create a common instance outside the loop as mappings shouldn't be changing while this is happening + Decompiler decompiler = decompilerService.create(compiled::get, new SourceSettings(false, false)); + + AtomicInteger count = new AtomicInteger(); + + Collection decompiled = classes.parallelStream() + .map(translatedNode -> { + progress.step(count.getAndIncrement(), translatedNode.name); + + String source = decompileClass(translatedNode, decompiler); + return new ClassSource(translatedNode.name, source); + }) + .collect(Collectors.toList()); + + return new SourceExport(decompiled); + } + + private String decompileClass(ClassNode translatedNode, Decompiler decompiler) { + return decompiler.getSource(translatedNode.name).asString(); + } + } + + public static final class SourceExport { + private final Collection decompiled; + + SourceExport(Collection decompiled) { + this.decompiled = decompiled; + } + + public void write(Path path, ProgressListener progress) throws IOException { + progress.init(decompiled.size(), I18n.translate("progress.sources.writing")); + + int count = 0; + for (ClassSource source : decompiled) { + progress.step(count++, source.name); + + Path sourcePath = source.resolvePath(path); + source.writeTo(sourcePath); + } + } + } + + private static class ClassSource { + private final String name; + private final String source; + + ClassSource(String name, String source) { + this.name = name; + this.source = source; + } + + void writeTo(Path path) throws IOException { + Files.createDirectories(path.getParent()); + try (BufferedWriter writer = Files.newBufferedWriter(path)) { + writer.write(source); + } + } + + Path resolvePath(Path root) { + return root.resolve(name.replace('.', '/') + ".java"); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/EnigmaServices.java b/enigma/src/main/java/cuchaz/enigma/EnigmaServices.java new file mode 100644 index 0000000..df3b7bb --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/EnigmaServices.java @@ -0,0 +1,20 @@ +package cuchaz.enigma; + +import com.google.common.collect.ImmutableListMultimap; +import cuchaz.enigma.api.service.EnigmaService; +import cuchaz.enigma.api.service.EnigmaServiceType; + +import java.util.List; + +public final class EnigmaServices { + private final ImmutableListMultimap, EnigmaService> services; + + EnigmaServices(ImmutableListMultimap, EnigmaService> services) { + this.services = services; + } + + @SuppressWarnings("unchecked") + public List get(EnigmaServiceType type) { + return (List) services.get(type); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/ProgressListener.java b/enigma/src/main/java/cuchaz/enigma/ProgressListener.java new file mode 100644 index 0000000..6da3b81 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/ProgressListener.java @@ -0,0 +1,19 @@ +package cuchaz.enigma; + +public interface ProgressListener { + static ProgressListener none() { + return new ProgressListener() { + @Override + public void init(int totalWork, String title) { + } + + @Override + public void step(int numDone, String message) { + } + }; + } + + void init(int totalWork, String title); + + void step(int numDone, String message); +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/Access.java b/enigma/src/main/java/cuchaz/enigma/analysis/Access.java new file mode 100644 index 0000000..82ca669 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/Access.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.analysis; + +import cuchaz.enigma.translation.representation.AccessFlags; + +import java.lang.reflect.Modifier; + +public enum Access { + + PUBLIC, PROTECTED, PACKAGE, PRIVATE; + + public static Access get(AccessFlags flags) { + return get(flags.getFlags()); + } + + public static Access get(int modifiers) { + boolean isPublic = Modifier.isPublic(modifiers); + boolean isProtected = Modifier.isProtected(modifiers); + boolean isPrivate = Modifier.isPrivate(modifiers); + + if (isPublic && !isProtected && !isPrivate) { + return PUBLIC; + } else if (!isPublic && isProtected && !isPrivate) { + return PROTECTED; + } else if (!isPublic && !isProtected && isPrivate) { + return PRIVATE; + } else if (!isPublic && !isProtected && !isPrivate) { + return PACKAGE; + } + // assume public by default + return PUBLIC; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java new file mode 100644 index 0000000..4685b39 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java @@ -0,0 +1,156 @@ +package cuchaz.enigma.analysis; + +import cuchaz.enigma.Enigma; +import cuchaz.enigma.api.EnigmaPlugin; +import cuchaz.enigma.api.EnigmaPluginContext; +import cuchaz.enigma.api.service.JarIndexerService; +import cuchaz.enigma.api.service.NameProposalService; +import cuchaz.enigma.source.DecompilerService; +import cuchaz.enigma.source.Decompilers; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.utils.Pair; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.SourceInterpreter; +import org.objectweb.asm.tree.analysis.SourceValue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public final class BuiltinPlugin implements EnigmaPlugin { + + public BuiltinPlugin() { + } + + @Override + public void init(EnigmaPluginContext ctx) { + registerEnumNamingService(ctx); + registerDecompilerServices(ctx); + } + + private void registerEnumNamingService(EnigmaPluginContext ctx) { + final Map, String> names = new HashMap<>(); + final EnumFieldNameFindingVisitor visitor = new EnumFieldNameFindingVisitor(names); + + ctx.registerService("enigma:enum_initializer_indexer", JarIndexerService.TYPE, ctx1 -> (classCache, jarIndex) -> classCache.visit(() -> visitor, ClassReader.SKIP_FRAMES)); + ctx.registerService("enigma:enum_name_proposer", NameProposalService.TYPE, ctx1 -> (obfEntry, remapper) -> Optional.ofNullable(names.get(obfEntry))); + } + + private void registerDecompilerServices(EnigmaPluginContext ctx) { + ctx.registerService("enigma:procyon", DecompilerService.TYPE, ctx1 -> Decompilers.PROCYON); + ctx.registerService("enigma:cfr", DecompilerService.TYPE, ctx1 -> Decompilers.CFR); + } + + private static final class EnumFieldNameFindingVisitor extends ClassVisitor { + + private ClassEntry clazz; + private String className; + private final Map, String> mappings; + private final Set> enumFields = new HashSet<>(); + private final List classInits = new ArrayList<>(); + + EnumFieldNameFindingVisitor(Map, String> mappings) { + super(Enigma.ASM_VERSION); + this.mappings = mappings; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + this.className = name; + this.clazz = new ClassEntry(name); + this.enumFields.clear(); + this.classInits.clear(); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + if ((access & Opcodes.ACC_ENUM) != 0) { + if (!enumFields.add(new Pair<>(name, descriptor))) { + throw new IllegalArgumentException("Found two enum fields with the same name \"" + name + "\" and desc \"" + descriptor + "\"!"); + } + } + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if ("".equals(name)) { + MethodNode node = new MethodNode(api, access, name, descriptor, signature, exceptions); + classInits.add(node); + return node; + } + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + @Override + public void visitEnd() { + super.visitEnd(); + try { + collectResults(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void collectResults() throws Exception { + String owner = className; + Analyzer analyzer = new Analyzer<>(new SourceInterpreter()); + + for (MethodNode mn : classInits) { + Frame[] frames = analyzer.analyze(className, mn); + + InsnList instrs = mn.instructions; + for (int i = 1; i < instrs.size(); i++) { + AbstractInsnNode instr1 = instrs.get(i - 1); + AbstractInsnNode instr2 = instrs.get(i); + String s = null; + + if (instr2.getOpcode() == Opcodes.PUTSTATIC + && ((FieldInsnNode) instr2).owner.equals(owner) + && enumFields.contains(new Pair<>(((FieldInsnNode) instr2).name, ((FieldInsnNode) instr2).desc)) + && instr1.getOpcode() == Opcodes.INVOKESPECIAL + && "".equals(((MethodInsnNode) instr1).name)) { + + for (int j = 0; j < frames[i - 1].getStackSize(); j++) { + SourceValue sv = frames[i - 1].getStack(j); + for (AbstractInsnNode ci : sv.insns) { + if (ci instanceof LdcInsnNode && ((LdcInsnNode) ci).cst instanceof String) { + //if (s == null || !s.equals(((LdcInsnNode) ci).cst)) { + if (s == null) { + s = (String) (((LdcInsnNode) ci).cst); + // stringsFound++; + } + } + } + } + } + + if (s != null) { + mappings.put(new FieldEntry(clazz, ((FieldInsnNode) instr2).name, new TypeDescriptor(((FieldInsnNode) instr2).desc)), s); + } + // report otherwise? + } + } + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/ClassCache.java b/enigma/src/main/java/cuchaz/enigma/analysis/ClassCache.java new file mode 100644 index 0000000..a3d24e3 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/ClassCache.java @@ -0,0 +1,126 @@ +package cuchaz.enigma.analysis; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableSet; +import cuchaz.enigma.ClassProvider; +import cuchaz.enigma.Enigma; +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.bytecode.translators.LocalVariableFixVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.tree.ClassNode; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public final class ClassCache implements AutoCloseable, ClassProvider { + private final FileSystem fileSystem; + private final ImmutableSet classNames; + + private final Cache nodeCache = CacheBuilder.newBuilder() + .maximumSize(128) + .expireAfterAccess(1, TimeUnit.MINUTES) + .build(); + + private ClassCache(FileSystem fileSystem, ImmutableSet classNames) { + this.fileSystem = fileSystem; + this.classNames = classNames; + } + + public static ClassCache of(Path jarPath) throws IOException { + FileSystem fileSystem = FileSystems.newFileSystem(jarPath, (ClassLoader) null); + ImmutableSet classNames = collectClassNames(fileSystem); + + return new ClassCache(fileSystem, classNames); + } + + private static ImmutableSet collectClassNames(FileSystem fileSystem) throws IOException { + ImmutableSet.Builder classNames = ImmutableSet.builder(); + for (Path root : fileSystem.getRootDirectories()) { + Files.walk(root).map(Path::toString) + .forEach(path -> { + if (path.endsWith(".class")) { + String name = path.substring(1, path.length() - ".class".length()); + classNames.add(name); + } + }); + } + + return classNames.build(); + } + + @Nullable + @Override + public ClassNode getClassNode(String name) { + if (!classNames.contains(name)) { + return null; + } + + try { + return nodeCache.get(name, () -> parseNode(name)); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private ClassNode parseNode(String name) throws IOException { + ClassReader reader = getReader(name); + + ClassNode node = new ClassNode(); + + LocalVariableFixVisitor visitor = new LocalVariableFixVisitor(Enigma.ASM_VERSION, node); + reader.accept(visitor, 0); + + return node; + } + + private ClassReader getReader(String name) throws IOException { + Path path = fileSystem.getPath(name + ".class"); + byte[] bytes = Files.readAllBytes(path); + return new ClassReader(bytes); + } + + public int getClassCount() { + return classNames.size(); + } + + public void visit(Supplier visitorSupplier, int readFlags) { + for (String className : classNames) { + ClassVisitor visitor = visitorSupplier.get(); + + ClassNode cached = nodeCache.getIfPresent(className); + if (cached != null) { + cached.accept(visitor); + continue; + } + + try { + ClassReader reader = getReader(className); + reader.accept(visitor, readFlags); + } catch (IOException e) { + System.out.println("Failed to visit class " + className); + e.printStackTrace(); + } + } + } + + @Override + public void close() throws IOException { + this.fileSystem.close(); + } + + public JarIndex index(ProgressListener progress) { + JarIndex index = JarIndex.empty(); + index.indexJar(this, progress); + return index; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/ClassImplementationsTreeNode.java b/enigma/src/main/java/cuchaz/enigma/analysis/ClassImplementationsTreeNode.java new file mode 100644 index 0000000..0fc44ca --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/ClassImplementationsTreeNode.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.analysis; + +import com.google.common.collect.Lists; +import cuchaz.enigma.analysis.index.InheritanceIndex; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import javax.swing.tree.DefaultMutableTreeNode; +import java.util.Collection; +import java.util.List; + +public class ClassImplementationsTreeNode extends DefaultMutableTreeNode { + private final Translator translator; + private final ClassEntry entry; + + public ClassImplementationsTreeNode(Translator translator, ClassEntry entry) { + this.translator = translator; + this.entry = entry; + } + + public static ClassImplementationsTreeNode findNode(ClassImplementationsTreeNode node, MethodEntry entry) { + // is this the node? + if (node.entry.equals(entry.getParent())) { + return node; + } + + // recurse + for (int i = 0; i < node.getChildCount(); i++) { + ClassImplementationsTreeNode foundNode = findNode((ClassImplementationsTreeNode) node.getChildAt(i), entry); + if (foundNode != null) { + return foundNode; + } + } + return null; + } + + public ClassEntry getClassEntry() { + return this.entry; + } + + @Override + public String toString() { + return translator.translate(entry).toString(); + } + + public void load(JarIndex index) { + // get all method implementations + List nodes = Lists.newArrayList(); + InheritanceIndex inheritanceIndex = index.getInheritanceIndex(); + + Collection inheritors = inheritanceIndex.getChildren(entry); + for (ClassEntry inheritor : inheritors) { + nodes.add(new ClassImplementationsTreeNode(translator, inheritor)); + } + + // add them to this node + nodes.forEach(this::add); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/ClassInheritanceTreeNode.java b/enigma/src/main/java/cuchaz/enigma/analysis/ClassInheritanceTreeNode.java new file mode 100644 index 0000000..7904c5f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/ClassInheritanceTreeNode.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.analysis; + +import com.google.common.collect.Lists; +import cuchaz.enigma.analysis.index.InheritanceIndex; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.entry.ClassEntry; + +import javax.swing.tree.DefaultMutableTreeNode; +import java.util.List; + +public class ClassInheritanceTreeNode extends DefaultMutableTreeNode { + private final Translator translator; + private final ClassEntry obfClassEntry; + + public ClassInheritanceTreeNode(Translator translator, String obfClassName) { + this.translator = translator; + this.obfClassEntry = new ClassEntry(obfClassName); + } + + public static ClassInheritanceTreeNode findNode(ClassInheritanceTreeNode node, ClassEntry entry) { + // is this the node? + if (node.getObfClassName().equals(entry.getFullName())) { + return node; + } + + // recurse + for (int i = 0; i < node.getChildCount(); i++) { + ClassInheritanceTreeNode foundNode = findNode((ClassInheritanceTreeNode) node.getChildAt(i), entry); + if (foundNode != null) { + return foundNode; + } + } + return null; + } + + public String getObfClassName() { + return this.obfClassEntry.getFullName(); + } + + @Override + public String toString() { + return translator.translate(obfClassEntry).getFullName(); + } + + public void load(InheritanceIndex ancestries, boolean recurse) { + // get all the child nodes + List nodes = Lists.newArrayList(); + for (ClassEntry inheritor : ancestries.getChildren(this.obfClassEntry)) { + nodes.add(new ClassInheritanceTreeNode(translator, inheritor.getFullName())); + } + + // add them to this node + nodes.forEach(this::add); + + if (recurse) { + for (ClassInheritanceTreeNode node : nodes) { + node.load(ancestries, true); + } + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/ClassReferenceTreeNode.java b/enigma/src/main/java/cuchaz/enigma/analysis/ClassReferenceTreeNode.java new file mode 100644 index 0000000..90d8a6c --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/ClassReferenceTreeNode.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.analysis; + +import com.google.common.collect.Sets; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.analysis.index.ReferenceIndex; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.MethodDefEntry; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeNode; +import java.util.Set; + +public class ClassReferenceTreeNode extends DefaultMutableTreeNode + implements ReferenceTreeNode { + + private Translator deobfuscatingTranslator; + private ClassEntry entry; + private EntryReference reference; + + public ClassReferenceTreeNode(Translator deobfuscatingTranslator, ClassEntry entry) { + this.deobfuscatingTranslator = deobfuscatingTranslator; + this.entry = entry; + this.reference = null; + } + + public ClassReferenceTreeNode(Translator deobfuscatingTranslator, EntryReference reference) { + this.deobfuscatingTranslator = deobfuscatingTranslator; + this.entry = reference.entry; + this.reference = reference; + } + + @Override + public ClassEntry getEntry() { + return this.entry; + } + + @Override + public EntryReference getReference() { + return this.reference; + } + + @Override + public String toString() { + if (this.reference != null) { + return String.format("%s", this.deobfuscatingTranslator.translate(this.reference.context)); + } + return this.deobfuscatingTranslator.translate(this.entry).getFullName(); + } + + public void load(JarIndex index, boolean recurse) { + ReferenceIndex referenceIndex = index.getReferenceIndex(); + + // get all the child nodes + for (EntryReference reference : referenceIndex.getReferencesToClass(this.entry)) { + add(new ClassReferenceTreeNode(this.deobfuscatingTranslator, reference)); + } + + if (recurse && this.children != null) { + for (Object child : this.children) { + if (child instanceof ClassReferenceTreeNode) { + ClassReferenceTreeNode node = (ClassReferenceTreeNode) child; + + // don't recurse into ancestor + Set> ancestors = Sets.newHashSet(); + TreeNode n = node; + while (n.getParent() != null) { + n = n.getParent(); + if (n instanceof ClassReferenceTreeNode) { + ancestors.add(((ClassReferenceTreeNode) n).getEntry()); + } + } + if (ancestors.contains(node.getEntry())) { + continue; + } + + node.load(index, true); + } + } + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java b/enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java new file mode 100644 index 0000000..320f945 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.analysis; + +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class EntryReference, C extends Entry> implements Translatable { + + private static final List CONSTRUCTOR_NON_NAMES = Arrays.asList("this", "super", "static"); + public E entry; + public C context; + public ReferenceTargetType targetType; + + private boolean sourceName; + + public EntryReference(E entry, String sourceName) { + this(entry, sourceName, null); + } + + public EntryReference(E entry, String sourceName, C context) { + this(entry, sourceName, context, ReferenceTargetType.none()); + } + + public EntryReference(E entry, String sourceName, C context, ReferenceTargetType targetType) { + if (entry == null) { + throw new IllegalArgumentException("Entry cannot be null!"); + } + + this.entry = entry; + this.context = context; + this.targetType = targetType; + + this.sourceName = sourceName != null && !sourceName.isEmpty(); + if (entry instanceof MethodEntry && ((MethodEntry) entry).isConstructor() && CONSTRUCTOR_NON_NAMES.contains(sourceName)) { + this.sourceName = false; + } + } + + public EntryReference(E entry, C context, EntryReference other) { + this.entry = entry; + this.context = context; + this.sourceName = other.sourceName; + this.targetType = other.targetType; + } + + public ClassEntry getLocationClassEntry() { + if (context != null) { + return context.getContainingClass(); + } + return entry.getContainingClass(); + } + + public boolean isNamed() { + return this.sourceName; + } + + public Entry getNameableEntry() { + if (entry instanceof MethodEntry && ((MethodEntry) entry).isConstructor()) { + // renaming a constructor really means renaming the class + return entry.getContainingClass(); + } + return entry; + } + + public String getNameableName() { + return getNameableEntry().getName(); + } + + @Override + public int hashCode() { + if (context != null) { + return Objects.hash(entry.hashCode(), context.hashCode()); + } + return entry.hashCode(); + } + + @Override + public boolean equals(Object other) { + return other instanceof EntryReference && equals((EntryReference) other); + } + + public boolean equals(EntryReference other) { + // check entry first + boolean isEntrySame = entry.equals(other.entry); + if (!isEntrySame) { + return false; + } + + // check caller + if (context == null && other.context == null) { + return true; + } else if (context != null && other.context != null) { + return context.equals(other.context); + } + return false; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(entry); + + if (context != null) { + buf.append(" called from "); + buf.append(context); + } + + if (targetType != null && targetType.getKind() != ReferenceTargetType.Kind.NONE) { + buf.append(" on target of type "); + buf.append(targetType); + } + + return buf.toString(); + } + + @Override + public Translatable translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + return new EntryReference<>(translator.translate(entry), translator.translate(context), this); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/FieldReferenceTreeNode.java b/enigma/src/main/java/cuchaz/enigma/analysis/FieldReferenceTreeNode.java new file mode 100644 index 0000000..4beab7f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/FieldReferenceTreeNode.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.analysis; + +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.analysis.index.ReferenceIndex; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodDefEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import javax.swing.tree.DefaultMutableTreeNode; + +public class FieldReferenceTreeNode extends DefaultMutableTreeNode implements ReferenceTreeNode { + + private final Translator translator; + private FieldEntry entry; + private EntryReference reference; + + public FieldReferenceTreeNode(Translator translator, FieldEntry entry) { + this.translator = translator; + this.entry = entry; + this.reference = null; + } + + private FieldReferenceTreeNode(Translator translator, EntryReference reference) { + this.translator = translator; + this.entry = reference.entry; + this.reference = reference; + } + + @Override + public FieldEntry getEntry() { + return this.entry; + } + + @Override + public EntryReference getReference() { + return this.reference; + } + + @Override + public String toString() { + if (this.reference != null) { + return String.format("%s", translator.translate(this.reference.context)); + } + return translator.translate(entry).toString(); + } + + public void load(JarIndex index, boolean recurse) { + ReferenceIndex referenceIndex = index.getReferenceIndex(); + + // get all the child nodes + if (this.reference == null) { + for (EntryReference reference : referenceIndex.getReferencesToField(this.entry)) { + add(new FieldReferenceTreeNode(translator, reference)); + } + } else { + for (EntryReference reference : referenceIndex.getReferencesToMethod(this.reference.context)) { + add(new MethodReferenceTreeNode(translator, reference)); + } + } + + if (recurse && children != null) { + for (Object node : children) { + if (node instanceof MethodReferenceTreeNode) { + ((MethodReferenceTreeNode) node).load(index, true, false); + } else if (node instanceof FieldReferenceTreeNode) { + ((FieldReferenceTreeNode) node).load(index, true); + } + } + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/IndexSimpleVerifier.java b/enigma/src/main/java/cuchaz/enigma/analysis/IndexSimpleVerifier.java new file mode 100644 index 0000000..ec8f323 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/IndexSimpleVerifier.java @@ -0,0 +1,154 @@ +package cuchaz.enigma.analysis; + +import cuchaz.enigma.Enigma; +import cuchaz.enigma.analysis.index.EntryIndex; +import cuchaz.enigma.analysis.index.InheritanceIndex; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.entry.ClassDefEntry; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.analysis.BasicValue; +import org.objectweb.asm.tree.analysis.SimpleVerifier; + +import java.util.Set; + +public class IndexSimpleVerifier extends SimpleVerifier { + private static final Type OBJECT_TYPE = Type.getType("Ljava/lang/Object;"); + private final EntryIndex entryIndex; + private final InheritanceIndex inheritanceIndex; + + public IndexSimpleVerifier(EntryIndex entryIndex, InheritanceIndex inheritanceIndex) { + super(Enigma.ASM_VERSION, null, null, null, false); + this.entryIndex = entryIndex; + this.inheritanceIndex = inheritanceIndex; + } + + @Override + protected boolean isSubTypeOf(BasicValue value, BasicValue expected) { + Type expectedType = expected.getType(); + Type type = value.getType(); + switch (expectedType.getSort()) { + case Type.INT: + case Type.FLOAT: + case Type.LONG: + case Type.DOUBLE: + return type.equals(expectedType); + case Type.ARRAY: + case Type.OBJECT: + if (type.equals(NULL_TYPE)) { + return true; + } else if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) { + if (isAssignableFrom(expectedType, type)) { + return true; + } else if (isInterface(expectedType)) { + return isAssignableFrom(OBJECT_TYPE, type); + } else { + return false; + } + } else { + return false; + } + default: + throw new AssertionError(); + } + } + + @Override + protected boolean isInterface(Type type) { + AccessFlags classAccess = entryIndex.getClassAccess(new ClassEntry(type.getInternalName())); + if (classAccess != null) { + return classAccess.isInterface(); + } + + Class clazz = getClass(type); + if (clazz != null) { + return clazz.isInterface(); + } + + return false; + } + + @Override + protected Type getSuperClass(Type type) { + ClassDefEntry definition = entryIndex.getDefinition(new ClassEntry(type.getInternalName())); + if (definition != null) { + return Type.getType('L' + definition.getSuperClass().getFullName() + ';'); + } + + Class clazz = getClass(type); + if (clazz != null) { + return Type.getType(clazz.getSuperclass()); + } + + return OBJECT_TYPE; + } + + @Override + protected boolean isAssignableFrom(Type type1, Type type2) { + if (type1.equals(type2)) { + return true; + } + + if (type2.equals(NULL_TYPE)) { + return true; + } + + if (type1.getSort() == Type.ARRAY) { + return type2.getSort() == Type.ARRAY && isAssignableFrom(Type.getType(type1.getDescriptor().substring(1)), Type.getType(type2.getDescriptor().substring(1))); + } + + if (type2.getSort() == Type.ARRAY) { + return type1.equals(OBJECT_TYPE); + } + + if (type1.getSort() == Type.OBJECT && type2.getSort() == Type.OBJECT) { + if (type1.equals(OBJECT_TYPE)) { + return true; + } + + ClassEntry class1 = new ClassEntry(type1.getInternalName()); + ClassEntry class2 = new ClassEntry(type2.getInternalName()); + + if (entryIndex.hasClass(class1) && entryIndex.hasClass(class2)) { + return inheritanceIndex.getAncestors(class2).contains(class1); + } + + Class class1Class = getClass(Type.getType('L' + class1.getFullName() + ';')); + Class class2Class = getClass(Type.getType('L' + class2.getFullName() + ';')); + + if (class1Class == null) { + return true; // missing classes to find out + } + + if (class2Class != null) { + return class1Class.isAssignableFrom(class2Class); + } + + if (entryIndex.hasClass(class2)) { + Set ancestors = inheritanceIndex.getAncestors(class2); + + for (ClassEntry ancestorEntry : ancestors) { + Class ancestor = getClass(Type.getType('L' + ancestorEntry.getFullName() + ';')); + if (ancestor == null || class1Class.isAssignableFrom(ancestor)) { + return true; // assignable, or missing classes to find out + } + } + + return false; + } + + return true; // missing classes to find out + } + + return false; + } + + @Override + protected final Class getClass(Type type) { + try { + return Class.forName(type.getSort() == Type.ARRAY ? type.getDescriptor().replace('/', '.') : type.getClassName(), false, null); + } catch (ClassNotFoundException e) { + return null; + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/IndexTreeBuilder.java b/enigma/src/main/java/cuchaz/enigma/analysis/IndexTreeBuilder.java new file mode 100644 index 0000000..0c2dfd7 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/IndexTreeBuilder.java @@ -0,0 +1,74 @@ +package cuchaz.enigma.analysis; + +import com.google.common.collect.Lists; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.util.Collection; +import java.util.List; + +public class IndexTreeBuilder { + private final JarIndex index; + + public IndexTreeBuilder(JarIndex index) { + this.index = index; + } + + public ClassInheritanceTreeNode buildClassInheritance(Translator translator, ClassEntry obfClassEntry) { + // get the root node + List ancestry = Lists.newArrayList(); + ancestry.add(obfClassEntry.getFullName()); + for (ClassEntry classEntry : index.getInheritanceIndex().getAncestors(obfClassEntry)) { + ancestry.add(classEntry.getFullName()); + } + + ClassInheritanceTreeNode rootNode = new ClassInheritanceTreeNode(translator, ancestry.get(ancestry.size() - 1)); + + // expand all children recursively + rootNode.load(index.getInheritanceIndex(), true); + + return rootNode; + } + + public ClassImplementationsTreeNode buildClassImplementations(Translator translator, ClassEntry obfClassEntry) { + if (index.getInheritanceIndex().isParent(obfClassEntry)) { + ClassImplementationsTreeNode node = new ClassImplementationsTreeNode(translator, obfClassEntry); + node.load(index); + return node; + } + return null; + } + + public MethodInheritanceTreeNode buildMethodInheritance(Translator translator, MethodEntry obfMethodEntry) { + MethodEntry resolvedEntry = index.getEntryResolver().resolveFirstEntry(obfMethodEntry, ResolutionStrategy.RESOLVE_ROOT); + + // make a root node at the base + MethodInheritanceTreeNode rootNode = new MethodInheritanceTreeNode( + translator, resolvedEntry, + index.getEntryIndex().hasMethod(resolvedEntry) + ); + + // expand the full tree + rootNode.load(index); + + return rootNode; + } + + public List buildMethodImplementations(Translator translator, MethodEntry obfMethodEntry) { + EntryResolver resolver = index.getEntryResolver(); + Collection resolvedEntries = resolver.resolveEntry(obfMethodEntry, ResolutionStrategy.RESOLVE_ROOT); + + List nodes = Lists.newArrayList(); + for (MethodEntry resolvedEntry : resolvedEntries) { + MethodImplementationsTreeNode node = new MethodImplementationsTreeNode(translator, resolvedEntry); + node.load(index); + nodes.add(node); + } + + return nodes; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/InterpreterPair.java b/enigma/src/main/java/cuchaz/enigma/analysis/InterpreterPair.java new file mode 100644 index 0000000..fad10bf --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/InterpreterPair.java @@ -0,0 +1,130 @@ +package cuchaz.enigma.analysis; + +import cuchaz.enigma.Enigma; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.Interpreter; +import org.objectweb.asm.tree.analysis.Value; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class InterpreterPair extends Interpreter> { + private final Interpreter left; + private final Interpreter right; + + public InterpreterPair(Interpreter left, Interpreter right) { + super(Enigma.ASM_VERSION); + this.left = left; + this.right = right; + } + + @Override + public PairValue newValue(Type type) { + return pair( + left.newValue(type), + right.newValue(type) + ); + } + + @Override + public PairValue newOperation(AbstractInsnNode insn) throws AnalyzerException { + return pair( + left.newOperation(insn), + right.newOperation(insn) + ); + } + + @Override + public PairValue copyOperation(AbstractInsnNode insn, PairValue value) throws AnalyzerException { + return pair( + left.copyOperation(insn, value.left), + right.copyOperation(insn, value.right) + ); + } + + @Override + public PairValue unaryOperation(AbstractInsnNode insn, PairValue value) throws AnalyzerException { + return pair( + left.unaryOperation(insn, value.left), + right.unaryOperation(insn, value.right) + ); + } + + @Override + public PairValue binaryOperation(AbstractInsnNode insn, PairValue value1, PairValue value2) throws AnalyzerException { + return pair( + left.binaryOperation(insn, value1.left, value2.left), + right.binaryOperation(insn, value1.right, value2.right) + ); + } + + @Override + public PairValue ternaryOperation(AbstractInsnNode insn, PairValue value1, PairValue value2, PairValue value3) throws AnalyzerException { + return pair( + left.ternaryOperation(insn, value1.left, value2.left, value3.left), + right.ternaryOperation(insn, value1.right, value2.right, value3.right) + ); + } + + @Override + public PairValue naryOperation(AbstractInsnNode insn, List> values) throws AnalyzerException { + return pair( + left.naryOperation(insn, values.stream().map(v -> v.left).collect(Collectors.toList())), + right.naryOperation(insn, values.stream().map(v -> v.right).collect(Collectors.toList())) + ); + } + + @Override + public void returnOperation(AbstractInsnNode insn, PairValue value, PairValue expected) throws AnalyzerException { + left.returnOperation(insn, value.left, expected.left); + right.returnOperation(insn, value.right, expected.right); + } + + @Override + public PairValue merge(PairValue value1, PairValue value2) { + return pair( + left.merge(value1.left, value2.left), + right.merge(value1.right, value2.right) + ); + } + + private PairValue pair(V left, W right) { + if (left == null && right == null) { + return null; + } + + return new PairValue<>(left, right); + } + + public static final class PairValue implements Value { + public final V left; + public final W right; + + public PairValue(V left, W right) { + if (left == null && right == null) { + throw new IllegalArgumentException("should use null rather than pair of nulls"); + } + + this.left = left; + this.right = right; + } + + @Override + public boolean equals(Object o) { + return o instanceof InterpreterPair.PairValue && Objects.equals(left, ((PairValue) o).left) && Objects.equals(right, ((PairValue) o).right); + } + + @Override + public int hashCode() { + return left.hashCode() * 31 + right.hashCode(); + } + + @Override + public int getSize() { + return (left == null ? right : left).getSize(); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/MethodImplementationsTreeNode.java b/enigma/src/main/java/cuchaz/enigma/analysis/MethodImplementationsTreeNode.java new file mode 100644 index 0000000..b09f7ac --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/MethodImplementationsTreeNode.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.analysis; + +import com.google.common.collect.Lists; +import cuchaz.enigma.analysis.index.EntryIndex; +import cuchaz.enigma.analysis.index.InheritanceIndex; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import javax.swing.tree.DefaultMutableTreeNode; +import java.util.Collection; +import java.util.List; + +public class MethodImplementationsTreeNode extends DefaultMutableTreeNode { + + private final Translator translator; + private MethodEntry entry; + + public MethodImplementationsTreeNode(Translator translator, MethodEntry entry) { + this.translator = translator; + if (entry == null) { + throw new IllegalArgumentException("Entry cannot be null!"); + } + + this.entry = entry; + } + + public static MethodImplementationsTreeNode findNode(MethodImplementationsTreeNode node, MethodEntry entry) { + // is this the node? + if (node.getMethodEntry().equals(entry)) { + return node; + } + + // recurse + for (int i = 0; i < node.getChildCount(); i++) { + MethodImplementationsTreeNode foundNode = findNode((MethodImplementationsTreeNode) node.getChildAt(i), entry); + if (foundNode != null) { + return foundNode; + } + } + return null; + } + + public MethodEntry getMethodEntry() { + return this.entry; + } + + @Override + public String toString() { + MethodEntry translatedEntry = translator.translate(entry); + String className = translatedEntry.getParent().getFullName(); + String methodName = translatedEntry.getName(); + return className + "." + methodName + "()"; + } + + public void load(JarIndex index) { + // get all method implementations + List nodes = Lists.newArrayList(); + EntryIndex entryIndex = index.getEntryIndex(); + InheritanceIndex inheritanceIndex = index.getInheritanceIndex(); + + Collection descendants = inheritanceIndex.getDescendants(entry.getParent()); + for (ClassEntry inheritor : descendants) { + MethodEntry methodEntry = entry.withParent(inheritor); + if (entryIndex.hasMethod(methodEntry)) { + nodes.add(new MethodImplementationsTreeNode(translator, methodEntry)); + } + } + + // add them to this node + nodes.forEach(this::add); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/MethodInheritanceTreeNode.java b/enigma/src/main/java/cuchaz/enigma/analysis/MethodInheritanceTreeNode.java new file mode 100644 index 0000000..e77b5cc --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/MethodInheritanceTreeNode.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.analysis; + +import cuchaz.enigma.analysis.index.EntryIndex; +import cuchaz.enigma.analysis.index.InheritanceIndex; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import javax.swing.tree.DefaultMutableTreeNode; + +public class MethodInheritanceTreeNode extends DefaultMutableTreeNode { + + private final Translator translator; + private MethodEntry entry; + private boolean implemented; + + public MethodInheritanceTreeNode(Translator translator, MethodEntry entry, boolean implemented) { + this.translator = translator; + this.entry = entry; + this.implemented = implemented; + } + + public static MethodInheritanceTreeNode findNode(MethodInheritanceTreeNode node, MethodEntry entry) { + // is this the node? + if (node.getMethodEntry().equals(entry)) { + return node; + } + + // recurse + for (int i = 0; i < node.getChildCount(); i++) { + MethodInheritanceTreeNode foundNode = findNode((MethodInheritanceTreeNode) node.getChildAt(i), entry); + if (foundNode != null) { + return foundNode; + } + } + return null; + } + + public MethodEntry getMethodEntry() { + return this.entry; + } + + public boolean isImplemented() { + return this.implemented; + } + + @Override + public String toString() { + MethodEntry translatedEntry = translator.translate(entry); + String className = translatedEntry.getContainingClass().getFullName(); + + if (!this.implemented) { + return className; + } else { + String methodName = translatedEntry.getName(); + return className + "." + methodName + "()"; + } + } + + /** + * Returns true if there is sub-node worthy to display. + */ + public boolean load(JarIndex index) { + // get all the child nodes + EntryIndex entryIndex = index.getEntryIndex(); + InheritanceIndex inheritanceIndex = index.getInheritanceIndex(); + + boolean ret = false; + for (ClassEntry inheritorEntry : inheritanceIndex.getChildren(this.entry.getParent())) { + MethodEntry methodEntry = new MethodEntry(inheritorEntry, this.entry.getName(), this.entry.getDesc()); + + MethodInheritanceTreeNode node = new MethodInheritanceTreeNode(translator, methodEntry, entryIndex.hasMethod(methodEntry)); + boolean childOverride = node.load(index); + + if (childOverride || node.implemented) { + this.add(node); + ret = true; + } + } + + return ret; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/MethodNodeWithAction.java b/enigma/src/main/java/cuchaz/enigma/analysis/MethodNodeWithAction.java new file mode 100644 index 0000000..8117103 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/MethodNodeWithAction.java @@ -0,0 +1,19 @@ +package cuchaz.enigma.analysis; + +import org.objectweb.asm.tree.MethodNode; + +import java.util.function.Consumer; + +public class MethodNodeWithAction extends MethodNode { + private final Consumer action; + + public MethodNodeWithAction(int api, int access, String name, String descriptor, String signature, String[] exceptions, Consumer action) { + super(api, access, name, descriptor, signature, exceptions); + this.action = action; + } + + @Override + public void visitEnd() { + action.accept(this); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/MethodReferenceTreeNode.java b/enigma/src/main/java/cuchaz/enigma/analysis/MethodReferenceTreeNode.java new file mode 100644 index 0000000..8995eb5 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/MethodReferenceTreeNode.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.analysis; + +import com.google.common.collect.Sets; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.analysis.index.ReferenceIndex; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.MethodDefEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeNode; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Set; + +public class MethodReferenceTreeNode extends DefaultMutableTreeNode implements ReferenceTreeNode { + + private final Translator translator; + private MethodEntry entry; + private EntryReference reference; + + public MethodReferenceTreeNode(Translator translator, MethodEntry entry) { + this.translator = translator; + this.entry = entry; + this.reference = null; + } + + public MethodReferenceTreeNode(Translator translator, EntryReference reference) { + this.translator = translator; + this.entry = reference.entry; + this.reference = reference; + } + + @Override + public MethodEntry getEntry() { + return this.entry; + } + + @Override + public EntryReference getReference() { + return this.reference; + } + + @Override + public String toString() { + if (this.reference != null) { + return String.format("%s", translator.translate(this.reference.context)); + } + return translator.translate(this.entry).getName(); + } + + public void load(JarIndex index, boolean recurse, boolean recurseMethod) { + // get all the child nodes + Collection> references = getReferences(index, recurseMethod); + + for (EntryReference reference : references) { + add(new MethodReferenceTreeNode(translator, reference)); + } + + if (recurse && this.children != null) { + for (Object child : this.children) { + if (child instanceof MethodReferenceTreeNode) { + MethodReferenceTreeNode node = (MethodReferenceTreeNode) child; + + // don't recurse into ancestor + Set> ancestors = Sets.newHashSet(); + TreeNode n = node; + while (n.getParent() != null) { + n = n.getParent(); + if (n instanceof MethodReferenceTreeNode) { + ancestors.add(((MethodReferenceTreeNode) n).getEntry()); + } + } + if (ancestors.contains(node.getEntry())) { + continue; + } + + node.load(index, true, false); + } + } + } + } + + private Collection> getReferences(JarIndex index, boolean recurseMethod) { + ReferenceIndex referenceIndex = index.getReferenceIndex(); + + if (recurseMethod) { + Collection> references = new ArrayList<>(); + + EntryResolver entryResolver = index.getEntryResolver(); + for (MethodEntry methodEntry : entryResolver.resolveEquivalentMethods(entry)) { + references.addAll(referenceIndex.getReferencesToMethod(methodEntry)); + } + + return references; + } else { + return referenceIndex.getReferencesToMethod(entry); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/ReferenceTargetType.java b/enigma/src/main/java/cuchaz/enigma/analysis/ReferenceTargetType.java new file mode 100644 index 0000000..5b19d18 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/ReferenceTargetType.java @@ -0,0 +1,74 @@ +package cuchaz.enigma.analysis; + +import cuchaz.enigma.translation.representation.entry.ClassEntry; + +public abstract class ReferenceTargetType { + private static final None NONE = new None(); + private static final Uninitialized UNINITIALIZED = new Uninitialized(); + + public abstract Kind getKind(); + + public static None none() { + return NONE; + } + + public static Uninitialized uninitialized() { + return UNINITIALIZED; + } + + public static ClassType classType(ClassEntry name) { + return new ClassType(name); + } + + public enum Kind { + NONE, + UNINITIALIZED, + CLASS_TYPE + } + + public static class None extends ReferenceTargetType { + @Override + public Kind getKind() { + return Kind.NONE; + } + + @Override + public String toString() { + return "(none)"; + } + } + + public static class Uninitialized extends ReferenceTargetType { + @Override + public Kind getKind() { + return Kind.UNINITIALIZED; + } + + @Override + public String toString() { + return "(uninitialized)"; + } + } + + public static class ClassType extends ReferenceTargetType { + private final ClassEntry entry; + + private ClassType(ClassEntry entry) { + this.entry = entry; + } + + public ClassEntry getEntry() { + return entry; + } + + @Override + public Kind getKind() { + return Kind.CLASS_TYPE; + } + + @Override + public String toString() { + return entry.toString(); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/ReferenceTreeNode.java b/enigma/src/main/java/cuchaz/enigma/analysis/ReferenceTreeNode.java new file mode 100644 index 0000000..c0a3a75 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/ReferenceTreeNode.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.analysis; + +import cuchaz.enigma.translation.representation.entry.Entry; + +public interface ReferenceTreeNode, C extends Entry> { + E getEntry(); + + EntryReference getReference(); +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/index/BridgeMethodIndex.java b/enigma/src/main/java/cuchaz/enigma/analysis/index/BridgeMethodIndex.java new file mode 100644 index 0000000..a4b1aac --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/index/BridgeMethodIndex.java @@ -0,0 +1,156 @@ +package cuchaz.enigma.analysis.index; + +import com.google.common.collect.Maps; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.MethodDefEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import javax.annotation.Nullable; +import java.util.*; + +public class BridgeMethodIndex implements JarIndexer { + private final EntryIndex entryIndex; + private final InheritanceIndex inheritanceIndex; + private final ReferenceIndex referenceIndex; + + private final Map bridgeToSpecialized = Maps.newHashMap(); + private final Map specializedToBridge = Maps.newHashMap(); + + public BridgeMethodIndex(EntryIndex entryIndex, InheritanceIndex inheritanceIndex, ReferenceIndex referenceIndex) { + this.entryIndex = entryIndex; + this.inheritanceIndex = inheritanceIndex; + this.referenceIndex = referenceIndex; + } + + public void findBridgeMethods() { + // look for access and bridged methods + for (MethodEntry methodEntry : entryIndex.getMethods()) { + MethodDefEntry methodDefEntry = (MethodDefEntry) methodEntry; + + AccessFlags access = methodDefEntry.getAccess(); + if (access == null || !access.isSynthetic()) { + continue; + } + + indexSyntheticMethod(methodDefEntry, access); + } + } + + @Override + public void processIndex(JarIndex index) { + Map copiedAccessToBridge = new HashMap<>(specializedToBridge); + + for (Map.Entry entry : copiedAccessToBridge.entrySet()) { + MethodEntry specializedEntry = entry.getKey(); + MethodEntry bridgeEntry = entry.getValue(); + if (bridgeEntry.getName().equals(specializedEntry.getName())) { + continue; + } + + MethodEntry renamedSpecializedEntry = specializedEntry.withName(bridgeEntry.getName()); + specializedToBridge.put(renamedSpecializedEntry, specializedToBridge.get(specializedEntry)); + } + } + + private void indexSyntheticMethod(MethodDefEntry syntheticMethod, AccessFlags access) { + MethodEntry specializedMethod = findSpecializedMethod(syntheticMethod); + if (specializedMethod == null) { + return; + } + + if (access.isBridge() || isPotentialBridge(syntheticMethod, specializedMethod)) { + bridgeToSpecialized.put(syntheticMethod, specializedMethod); + specializedToBridge.put(specializedMethod, syntheticMethod); + } + } + + private MethodEntry findSpecializedMethod(MethodEntry method) { + // we want to find all compiler-added methods that directly call another with no processing + + // get all the methods that we call + final Collection referencedMethods = referenceIndex.getMethodsReferencedBy(method); + + // is there just one? + if (referencedMethods.size() != 1) { + return null; + } + + return referencedMethods.stream().findFirst().orElse(null); + } + + private boolean isPotentialBridge(MethodDefEntry bridgeMethod, MethodEntry specializedMethod) { + // Bridge methods only exist for inheritance purposes, if we're private, final, or static, we cannot be inherited + AccessFlags bridgeAccess = bridgeMethod.getAccess(); + if (bridgeAccess.isPrivate() || bridgeAccess.isFinal() || bridgeAccess.isStatic()) { + return false; + } + + MethodDescriptor bridgeDesc = bridgeMethod.getDesc(); + MethodDescriptor specializedDesc = specializedMethod.getDesc(); + List bridgeArguments = bridgeDesc.getArgumentDescs(); + List specializedArguments = specializedDesc.getArgumentDescs(); + + // A bridge method will always have the same number of arguments + if (bridgeArguments.size() != specializedArguments.size()) { + return false; + } + + // Check that all argument types are bridge-compatible + for (int i = 0; i < bridgeArguments.size(); i++) { + if (!areTypesBridgeCompatible(bridgeArguments.get(i), specializedArguments.get(i))) { + return false; + } + } + + // Check that the return type is bridge-compatible + return areTypesBridgeCompatible(bridgeDesc.getReturnDesc(), specializedDesc.getReturnDesc()); + } + + private boolean areTypesBridgeCompatible(TypeDescriptor bridgeDesc, TypeDescriptor specializedDesc) { + if (bridgeDesc.equals(specializedDesc)) { + return true; + } + + // Either the descs will be equal, or they are both types and different through a generic + if (bridgeDesc.isType() && specializedDesc.isType()) { + ClassEntry bridgeType = bridgeDesc.getTypeEntry(); + ClassEntry accessedType = specializedDesc.getTypeEntry(); + + // If the given types are completely unrelated to each other, this can't be bridge compatible + InheritanceIndex.Relation relation = inheritanceIndex.computeClassRelation(accessedType, bridgeType); + return relation != InheritanceIndex.Relation.UNRELATED; + } + + return false; + } + + public boolean isBridgeMethod(MethodEntry entry) { + return bridgeToSpecialized.containsKey(entry); + } + + public boolean isSpecializedMethod(MethodEntry entry) { + return specializedToBridge.containsKey(entry); + } + + @Nullable + public MethodEntry getBridgeFromSpecialized(MethodEntry specialized) { + return specializedToBridge.get(specialized); + } + + public MethodEntry getSpecializedFromBridge(MethodEntry bridge) { + return bridgeToSpecialized.get(bridge); + } + + /** Includes "renamed specialized -> bridge" entries. */ + public Map getSpecializedToBridge() { + return Collections.unmodifiableMap(specializedToBridge); + } + + /** Only "bridge -> original name" entries. **/ + public Map getBridgeToSpecialized() { + return Collections.unmodifiableMap(bridgeToSpecialized); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/index/EntryIndex.java b/enigma/src/main/java/cuchaz/enigma/analysis/index/EntryIndex.java new file mode 100644 index 0000000..9a2726e --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/index/EntryIndex.java @@ -0,0 +1,102 @@ +package cuchaz.enigma.analysis.index; + +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.entry.*; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class EntryIndex implements JarIndexer { + private Map classes = new HashMap<>(); + private Map fields = new HashMap<>(); + private Map methods = new HashMap<>(); + private Map definitions = new HashMap<>(); + + @Override + public void indexClass(ClassDefEntry classEntry) { + definitions.put(classEntry, classEntry); + classes.put(classEntry, classEntry.getAccess()); + } + + @Override + public void indexMethod(MethodDefEntry methodEntry) { + methods.put(methodEntry, methodEntry.getAccess()); + } + + @Override + public void indexField(FieldDefEntry fieldEntry) { + fields.put(fieldEntry, fieldEntry.getAccess()); + } + + public boolean hasClass(ClassEntry entry) { + return classes.containsKey(entry); + } + + public boolean hasMethod(MethodEntry entry) { + return methods.containsKey(entry); + } + + public boolean hasField(FieldEntry entry) { + return fields.containsKey(entry); + } + + public boolean hasEntry(Entry entry) { + if (entry instanceof ClassEntry) { + return hasClass((ClassEntry) entry); + } else if (entry instanceof MethodEntry) { + return hasMethod((MethodEntry) entry); + } else if (entry instanceof FieldEntry) { + return hasField((FieldEntry) entry); + } else if (entry instanceof LocalVariableEntry) { + return hasMethod(((LocalVariableEntry) entry).getParent()); + } + + return false; + } + + @Nullable + public AccessFlags getMethodAccess(MethodEntry entry) { + return methods.get(entry); + } + + @Nullable + public AccessFlags getFieldAccess(FieldEntry entry) { + return fields.get(entry); + } + + @Nullable + public AccessFlags getClassAccess(ClassEntry entry) { + return classes.get(entry); + } + + @Nullable + public AccessFlags getEntryAccess(Entry entry) { + if (entry instanceof MethodEntry) { + return getMethodAccess((MethodEntry) entry); + } else if (entry instanceof FieldEntry) { + return getFieldAccess((FieldEntry) entry); + } else if (entry instanceof LocalVariableEntry) { + return getMethodAccess(((LocalVariableEntry) entry).getParent()); + } + + return null; + } + + public ClassDefEntry getDefinition(ClassEntry entry) { + return definitions.get(entry); + } + + public Collection getClasses() { + return classes.keySet(); + } + + public Collection getMethods() { + return methods.keySet(); + } + + public Collection getFields() { + return fields.keySet(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/index/IndexClassVisitor.java b/enigma/src/main/java/cuchaz/enigma/analysis/index/IndexClassVisitor.java new file mode 100644 index 0000000..f9cb23c --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/index/IndexClassVisitor.java @@ -0,0 +1,40 @@ +package cuchaz.enigma.analysis.index; + +import cuchaz.enigma.translation.representation.entry.ClassDefEntry; +import cuchaz.enigma.translation.representation.entry.FieldDefEntry; +import cuchaz.enigma.translation.representation.entry.MethodDefEntry; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; + +public class IndexClassVisitor extends ClassVisitor { + private final JarIndexer indexer; + private ClassDefEntry classEntry; + + public IndexClassVisitor(JarIndex indexer, int api) { + super(api); + this.indexer = indexer; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + classEntry = ClassDefEntry.parse(access, name, signature, superName, interfaces); + indexer.indexClass(classEntry); + + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + indexer.indexField(FieldDefEntry.parse(classEntry, access, name, desc, signature)); + + return super.visitField(access, name, desc, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + indexer.indexMethod(MethodDefEntry.parse(classEntry, access, name, desc, signature)); + + return super.visitMethod(access, name, desc, signature, exceptions); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/index/IndexReferenceVisitor.java b/enigma/src/main/java/cuchaz/enigma/analysis/index/IndexReferenceVisitor.java new file mode 100644 index 0000000..f3d419e --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/index/IndexReferenceVisitor.java @@ -0,0 +1,180 @@ +package cuchaz.enigma.analysis.index; + +import cuchaz.enigma.analysis.IndexSimpleVerifier; +import cuchaz.enigma.analysis.InterpreterPair; +import cuchaz.enigma.analysis.MethodNodeWithAction; +import cuchaz.enigma.analysis.ReferenceTargetType; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.Lambda; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.Signature; +import cuchaz.enigma.translation.representation.entry.*; +import org.objectweb.asm.*; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.analysis.*; + +import java.util.List; +import java.util.stream.Collectors; + +public class IndexReferenceVisitor extends ClassVisitor { + private final JarIndexer indexer; + private final EntryIndex entryIndex; + private final InheritanceIndex inheritanceIndex; + private ClassEntry classEntry; + private String className; + + public IndexReferenceVisitor(JarIndexer indexer, EntryIndex entryIndex, InheritanceIndex inheritanceIndex, int api) { + super(api); + this.indexer = indexer; + this.entryIndex = entryIndex; + this.inheritanceIndex = inheritanceIndex; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + classEntry = new ClassEntry(name); + className = name; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + MethodDefEntry entry = new MethodDefEntry(classEntry, name, new MethodDescriptor(desc), Signature.createSignature(signature), new AccessFlags(access)); + return new MethodNodeWithAction(api, access, name, desc, signature, exceptions, methodNode -> { + try { + new Analyzer<>(new MethodInterpreter(entry, indexer, entryIndex, inheritanceIndex)).analyze(className, methodNode); + } catch (AnalyzerException e) { + throw new RuntimeException(e); + } + }); + } + + private static class MethodInterpreter extends InterpreterPair { + private final MethodDefEntry callerEntry; + private JarIndexer indexer; + + public MethodInterpreter(MethodDefEntry callerEntry, JarIndexer indexer, EntryIndex entryIndex, InheritanceIndex inheritanceIndex) { + super(new IndexSimpleVerifier(entryIndex, inheritanceIndex), new SourceInterpreter()); + this.callerEntry = callerEntry; + this.indexer = indexer; + } + + @Override + public PairValue newOperation(AbstractInsnNode insn) throws AnalyzerException { + if (insn.getOpcode() == Opcodes.GETSTATIC) { + FieldInsnNode field = (FieldInsnNode) insn; + indexer.indexFieldReference(callerEntry, FieldEntry.parse(field.owner, field.name, field.desc), ReferenceTargetType.none()); + } + + return super.newOperation(insn); + } + + @Override + public PairValue unaryOperation(AbstractInsnNode insn, PairValue value) throws AnalyzerException { + if (insn.getOpcode() == Opcodes.PUTSTATIC) { + FieldInsnNode field = (FieldInsnNode) insn; + indexer.indexFieldReference(callerEntry, FieldEntry.parse(field.owner, field.name, field.desc), ReferenceTargetType.none()); + } + + if (insn.getOpcode() == Opcodes.GETFIELD) { + FieldInsnNode field = (FieldInsnNode) insn; + indexer.indexFieldReference(callerEntry, FieldEntry.parse(field.owner, field.name, field.desc), getReferenceTargetType(value, insn)); + } + + return super.unaryOperation(insn, value); + } + + + @Override + public PairValue binaryOperation(AbstractInsnNode insn, PairValue value1, PairValue value2) throws AnalyzerException { + if (insn.getOpcode() == Opcodes.PUTFIELD) { + FieldInsnNode field = (FieldInsnNode) insn; + FieldEntry fieldEntry = FieldEntry.parse(field.owner, field.name, field.desc); + indexer.indexFieldReference(callerEntry, fieldEntry, ReferenceTargetType.none()); + } + + return super.binaryOperation(insn, value1, value2); + } + + @Override + public PairValue naryOperation(AbstractInsnNode insn, List> values) throws AnalyzerException { + if (insn.getOpcode() == Opcodes.INVOKEINTERFACE || insn.getOpcode() == Opcodes.INVOKESPECIAL || insn.getOpcode() == Opcodes.INVOKEVIRTUAL) { + MethodInsnNode methodInsn = (MethodInsnNode) insn; + indexer.indexMethodReference(callerEntry, MethodEntry.parse(methodInsn.owner, methodInsn.name, methodInsn.desc), getReferenceTargetType(values.get(0), insn)); + } + + if (insn.getOpcode() == Opcodes.INVOKESTATIC) { + MethodInsnNode methodInsn = (MethodInsnNode) insn; + indexer.indexMethodReference(callerEntry, MethodEntry.parse(methodInsn.owner, methodInsn.name, methodInsn.desc), ReferenceTargetType.none()); + } + + if (insn.getOpcode() == Opcodes.INVOKEDYNAMIC) { + InvokeDynamicInsnNode invokeDynamicInsn = (InvokeDynamicInsnNode) insn; + List args = values.stream().map(v -> v.right.insns.stream().findFirst().orElseThrow(AssertionError::new)).collect(Collectors.toList()); + + if ("java/lang/invoke/LambdaMetafactory".equals(invokeDynamicInsn.bsm.getOwner()) && "metafactory".equals(invokeDynamicInsn.bsm.getName())) { + Type samMethodType = (Type) invokeDynamicInsn.bsmArgs[0]; + Handle implMethod = (Handle) invokeDynamicInsn.bsmArgs[1]; + Type instantiatedMethodType = (Type) invokeDynamicInsn.bsmArgs[2]; + + ReferenceTargetType targetType; + if (implMethod.getTag() != Opcodes.H_GETSTATIC && implMethod.getTag() != Opcodes.H_PUTFIELD && implMethod.getTag() != Opcodes.H_INVOKESTATIC) { + if (instantiatedMethodType.getArgumentTypes().length < Type.getArgumentTypes(implMethod.getDesc()).length) { + targetType = getReferenceTargetType(values.get(0), insn); + } else { + targetType = ReferenceTargetType.none(); // no "this" argument + } + } else { + targetType = ReferenceTargetType.none(); + } + + indexer.indexLambda(callerEntry, new Lambda( + invokeDynamicInsn.name, + new MethodDescriptor(invokeDynamicInsn.desc), + new MethodDescriptor(samMethodType.getDescriptor()), + getHandleEntry(implMethod), + new MethodDescriptor(instantiatedMethodType.getDescriptor()) + ), targetType); + } + } + + return super.naryOperation(insn, values); + } + + private ReferenceTargetType getReferenceTargetType(PairValue target, AbstractInsnNode insn) throws AnalyzerException { + if (target.left == BasicValue.UNINITIALIZED_VALUE) { + return ReferenceTargetType.uninitialized(); + } + + if (target.left.getType().getSort() == Type.OBJECT) { + return ReferenceTargetType.classType(new ClassEntry(target.left.getType().getInternalName())); + } + + if (target.left.getType().getSort() == Type.ARRAY) { + return ReferenceTargetType.classType(new ClassEntry("java/lang/Object")); + } + + throw new AnalyzerException(insn, "called method on or accessed field of non-object type"); + } + + private static ParentedEntry getHandleEntry(Handle handle) { + switch (handle.getTag()) { + case Opcodes.H_GETFIELD: + case Opcodes.H_GETSTATIC: + case Opcodes.H_PUTFIELD: + case Opcodes.H_PUTSTATIC: + return FieldEntry.parse(handle.getOwner(), handle.getName(), handle.getDesc()); + case Opcodes.H_INVOKEINTERFACE: + case Opcodes.H_INVOKESPECIAL: + case Opcodes.H_INVOKESTATIC: + case Opcodes.H_INVOKEVIRTUAL: + case Opcodes.H_NEWINVOKESPECIAL: + return MethodEntry.parse(handle.getOwner(), handle.getName(), handle.getDesc()); + } + + throw new RuntimeException("Invalid handle tag " + handle.getTag()); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/index/InheritanceIndex.java b/enigma/src/main/java/cuchaz/enigma/analysis/index/InheritanceIndex.java new file mode 100644 index 0000000..1ab2abd --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/index/InheritanceIndex.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.analysis.index; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import cuchaz.enigma.translation.representation.entry.ClassDefEntry; +import cuchaz.enigma.translation.representation.entry.ClassEntry; + +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Set; + +public class InheritanceIndex implements JarIndexer { + private final EntryIndex entryIndex; + + private Multimap classParents = HashMultimap.create(); + private Multimap classChildren = HashMultimap.create(); + + public InheritanceIndex(EntryIndex entryIndex) { + this.entryIndex = entryIndex; + } + + @Override + public void indexClass(ClassDefEntry classEntry) { + if (classEntry.isJre()) { + return; + } + + ClassEntry superClass = classEntry.getSuperClass(); + if (superClass != null && !superClass.getName().equals("java/lang/Object")) { + indexParent(classEntry, superClass); + } + + for (ClassEntry interfaceEntry : classEntry.getInterfaces()) { + indexParent(classEntry, interfaceEntry); + } + } + + private void indexParent(ClassEntry childEntry, ClassEntry parentEntry) { + classParents.put(childEntry, parentEntry); + classChildren.put(parentEntry, childEntry); + } + + public Collection getParents(ClassEntry classEntry) { + return classParents.get(classEntry); + } + + public Collection getChildren(ClassEntry classEntry) { + return classChildren.get(classEntry); + } + + public Collection getDescendants(ClassEntry classEntry) { + Collection descendants = new HashSet<>(); + + LinkedList descendantQueue = new LinkedList<>(); + descendantQueue.push(classEntry); + + while (!descendantQueue.isEmpty()) { + ClassEntry descendant = descendantQueue.pop(); + Collection children = getChildren(descendant); + + children.forEach(descendantQueue::push); + descendants.addAll(children); + } + + return descendants; + } + + public Set getAncestors(ClassEntry classEntry) { + Set ancestors = Sets.newHashSet(); + + LinkedList ancestorQueue = new LinkedList<>(); + ancestorQueue.push(classEntry); + + while (!ancestorQueue.isEmpty()) { + ClassEntry ancestor = ancestorQueue.pop(); + Collection parents = getParents(ancestor); + + parents.forEach(ancestorQueue::push); + ancestors.addAll(parents); + } + + return ancestors; + } + + public Relation computeClassRelation(ClassEntry classEntry, ClassEntry potentialAncestor) { + if (potentialAncestor.getName().equals("java/lang/Object")) return Relation.RELATED; + if (!entryIndex.hasClass(classEntry)) return Relation.UNKNOWN; + + for (ClassEntry ancestor : getAncestors(classEntry)) { + if (potentialAncestor.equals(ancestor)) { + return Relation.RELATED; + } else if (!entryIndex.hasClass(ancestor)) { + return Relation.UNKNOWN; + } + } + + return Relation.UNRELATED; + } + + public boolean isParent(ClassEntry classEntry) { + return classChildren.containsKey(classEntry); + } + + public boolean hasParents(ClassEntry classEntry) { + Collection parents = classParents.get(classEntry); + return parents != null && !parents.isEmpty(); + } + + public enum Relation { + RELATED, + UNRELATED, + UNKNOWN + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/index/JarIndex.java b/enigma/src/main/java/cuchaz/enigma/analysis/index/JarIndex.java new file mode 100644 index 0000000..de3f5f5 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/index/JarIndex.java @@ -0,0 +1,170 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.analysis.index; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import cuchaz.enigma.Enigma; +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.analysis.ClassCache; +import cuchaz.enigma.analysis.ReferenceTargetType; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.IndexEntryResolver; +import cuchaz.enigma.translation.representation.Lambda; +import cuchaz.enigma.translation.representation.entry.*; +import cuchaz.enigma.utils.I18n; + +import org.objectweb.asm.ClassReader; + +import java.util.Arrays; +import java.util.Collection; + +public class JarIndex implements JarIndexer { + private final EntryIndex entryIndex; + private final InheritanceIndex inheritanceIndex; + private final ReferenceIndex referenceIndex; + private final BridgeMethodIndex bridgeMethodIndex; + private final PackageVisibilityIndex packageVisibilityIndex; + private final EntryResolver entryResolver; + + private final Collection indexers; + + private final Multimap methodImplementations = HashMultimap.create(); + + public JarIndex(EntryIndex entryIndex, InheritanceIndex inheritanceIndex, ReferenceIndex referenceIndex, BridgeMethodIndex bridgeMethodIndex, PackageVisibilityIndex packageVisibilityIndex) { + this.entryIndex = entryIndex; + this.inheritanceIndex = inheritanceIndex; + this.referenceIndex = referenceIndex; + this.bridgeMethodIndex = bridgeMethodIndex; + this.packageVisibilityIndex = packageVisibilityIndex; + this.indexers = Arrays.asList(entryIndex, inheritanceIndex, referenceIndex, bridgeMethodIndex, packageVisibilityIndex); + this.entryResolver = new IndexEntryResolver(this); + } + + public static JarIndex empty() { + EntryIndex entryIndex = new EntryIndex(); + InheritanceIndex inheritanceIndex = new InheritanceIndex(entryIndex); + ReferenceIndex referenceIndex = new ReferenceIndex(); + BridgeMethodIndex bridgeMethodIndex = new BridgeMethodIndex(entryIndex, inheritanceIndex, referenceIndex); + PackageVisibilityIndex packageVisibilityIndex = new PackageVisibilityIndex(); + return new JarIndex(entryIndex, inheritanceIndex, referenceIndex, bridgeMethodIndex, packageVisibilityIndex); + } + + public void indexJar(ClassCache classCache, ProgressListener progress) { + progress.init(4, I18n.translate("progress.jar.indexing")); + + progress.step(1, I18n.translate("progress.jar.indexing.entries")); + classCache.visit(() -> new IndexClassVisitor(this, Enigma.ASM_VERSION), ClassReader.SKIP_CODE); + + progress.step(2, I18n.translate("progress.jar.indexing.references")); + classCache.visit(() -> new IndexReferenceVisitor(this, entryIndex, inheritanceIndex, Enigma.ASM_VERSION), 0); + + progress.step(3, I18n.translate("progress.jar.indexing.methods")); + bridgeMethodIndex.findBridgeMethods(); + + progress.step(4, I18n.translate("progress.jar.indexing.process")); + processIndex(this); + } + + @Override + public void processIndex(JarIndex index) { + indexers.forEach(indexer -> indexer.processIndex(index)); + } + + @Override + public void indexClass(ClassDefEntry classEntry) { + if (classEntry.isJre()) { + return; + } + + for (ClassEntry interfaceEntry : classEntry.getInterfaces()) { + if (classEntry.equals(interfaceEntry)) { + throw new IllegalArgumentException("Class cannot be its own interface! " + classEntry); + } + } + + indexers.forEach(indexer -> indexer.indexClass(classEntry)); + } + + @Override + public void indexField(FieldDefEntry fieldEntry) { + if (fieldEntry.getParent().isJre()) { + return; + } + + indexers.forEach(indexer -> indexer.indexField(fieldEntry)); + } + + @Override + public void indexMethod(MethodDefEntry methodEntry) { + if (methodEntry.getParent().isJre()) { + return; + } + + indexers.forEach(indexer -> indexer.indexMethod(methodEntry)); + + if (!methodEntry.isConstructor()) { + methodImplementations.put(methodEntry.getParent().getFullName(), methodEntry); + } + } + + @Override + public void indexMethodReference(MethodDefEntry callerEntry, MethodEntry referencedEntry, ReferenceTargetType targetType) { + if (callerEntry.getParent().isJre()) { + return; + } + + indexers.forEach(indexer -> indexer.indexMethodReference(callerEntry, referencedEntry, targetType)); + } + + @Override + public void indexFieldReference(MethodDefEntry callerEntry, FieldEntry referencedEntry, ReferenceTargetType targetType) { + if (callerEntry.getParent().isJre()) { + return; + } + + indexers.forEach(indexer -> indexer.indexFieldReference(callerEntry, referencedEntry, targetType)); + } + + @Override + public void indexLambda(MethodDefEntry callerEntry, Lambda lambda, ReferenceTargetType targetType) { + if (callerEntry.getParent().isJre()) { + return; + } + + indexers.forEach(indexer -> indexer.indexLambda(callerEntry, lambda, targetType)); + } + + public EntryIndex getEntryIndex() { + return entryIndex; + } + + public InheritanceIndex getInheritanceIndex() { + return this.inheritanceIndex; + } + + public ReferenceIndex getReferenceIndex() { + return referenceIndex; + } + + public BridgeMethodIndex getBridgeMethodIndex() { + return bridgeMethodIndex; + } + + public PackageVisibilityIndex getPackageVisibilityIndex() { + return packageVisibilityIndex; + } + + public EntryResolver getEntryResolver() { + return entryResolver; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/index/JarIndexer.java b/enigma/src/main/java/cuchaz/enigma/analysis/index/JarIndexer.java new file mode 100644 index 0000000..f17e7c9 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/index/JarIndexer.java @@ -0,0 +1,28 @@ +package cuchaz.enigma.analysis.index; + +import cuchaz.enigma.analysis.ReferenceTargetType; +import cuchaz.enigma.translation.representation.Lambda; +import cuchaz.enigma.translation.representation.entry.*; + +public interface JarIndexer { + default void indexClass(ClassDefEntry classEntry) { + } + + default void indexField(FieldDefEntry fieldEntry) { + } + + default void indexMethod(MethodDefEntry methodEntry) { + } + + default void indexMethodReference(MethodDefEntry callerEntry, MethodEntry referencedEntry, ReferenceTargetType targetType) { + } + + default void indexFieldReference(MethodDefEntry callerEntry, FieldEntry referencedEntry, ReferenceTargetType targetType) { + } + + default void indexLambda(MethodDefEntry callerEntry, Lambda lambda, ReferenceTargetType targetType) { + } + + default void processIndex(JarIndex index) { + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/index/PackageVisibilityIndex.java b/enigma/src/main/java/cuchaz/enigma/analysis/index/PackageVisibilityIndex.java new file mode 100644 index 0000000..64de5f3 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/index/PackageVisibilityIndex.java @@ -0,0 +1,147 @@ +package cuchaz.enigma.analysis.index; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.analysis.ReferenceTargetType; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.entry.*; + +import java.util.*; + +public class PackageVisibilityIndex implements JarIndexer { + private static boolean requiresSamePackage(AccessFlags entryAcc, EntryReference ref, InheritanceIndex inheritanceIndex) { + if (entryAcc.isPublic()) { + return false; + } + + if (entryAcc.isProtected()) { + ClassEntry contextClass = ref.context.getContainingClass(); + ClassEntry referencedClass = ref.entry.getContainingClass(); + + if (!inheritanceIndex.getAncestors(contextClass).contains(referencedClass)) { + return true; // access to protected member not in superclass + } + + if (ref.targetType.getKind() == ReferenceTargetType.Kind.NONE) { + return false; // access to superclass or static superclass member + } + + // access to instance member only valid if target's class assignable to context class + return !(ref.targetType.getKind() == ReferenceTargetType.Kind.UNINITIALIZED || + ((ReferenceTargetType.ClassType) ref.targetType).getEntry().equals(contextClass) || + inheritanceIndex.getAncestors(((ReferenceTargetType.ClassType) ref.targetType).getEntry()).contains(contextClass)); + } + + return true; + } + + private final HashMultimap connections = HashMultimap.create(); + private final List> partitions = Lists.newArrayList(); + private final Map> classPartitions = Maps.newHashMap(); + + private void addConnection(ClassEntry classA, ClassEntry classB) { + if (classA != classB) { + connections.put(classA, classB); + connections.put(classB, classA); + } + } + + private void buildPartition(Set unassignedClasses, Set partition, ClassEntry member) { + for (ClassEntry connected : connections.get(member)) { + if (unassignedClasses.remove(connected)) { + partition.add(connected); + buildPartition(unassignedClasses, partition, connected); + } + } + } + + private void addConnections(EntryIndex entryIndex, ReferenceIndex referenceIndex, InheritanceIndex inheritanceIndex) { + for (FieldEntry entry : entryIndex.getFields()) { + AccessFlags entryAcc = entryIndex.getFieldAccess(entry); + if (!entryAcc.isPublic() && !entryAcc.isPrivate()) { + for (EntryReference ref : referenceIndex.getReferencesToField(entry)) { + if (requiresSamePackage(entryAcc, ref, inheritanceIndex)) { + addConnection(ref.entry.getContainingClass(), ref.context.getContainingClass()); + } + } + } + } + + for (MethodEntry entry : entryIndex.getMethods()) { + AccessFlags entryAcc = entryIndex.getMethodAccess(entry); + if (!entryAcc.isPublic() && !entryAcc.isPrivate()) { + for (EntryReference ref : referenceIndex.getReferencesToMethod(entry)) { + if (requiresSamePackage(entryAcc, ref, inheritanceIndex)) { + addConnection(ref.entry.getContainingClass(), ref.context.getContainingClass()); + } + } + } + } + + for (ClassEntry entry : entryIndex.getClasses()) { + AccessFlags entryAcc = entryIndex.getClassAccess(entry); + if (!entryAcc.isPublic() && !entryAcc.isPrivate()) { + for (EntryReference ref : referenceIndex.getFieldTypeReferencesToClass(entry)) { + if (requiresSamePackage(entryAcc, ref, inheritanceIndex)) { + addConnection(ref.entry.getContainingClass(), ref.context.getContainingClass()); + } + } + + for (EntryReference ref : referenceIndex.getMethodTypeReferencesToClass(entry)) { + if (requiresSamePackage(entryAcc, ref, inheritanceIndex)) { + addConnection(ref.entry.getContainingClass(), ref.context.getContainingClass()); + } + } + } + + for (ClassEntry parent : inheritanceIndex.getParents(entry)) { + AccessFlags parentAcc = entryIndex.getClassAccess(parent); + if (parentAcc != null && !parentAcc.isPublic() && !parentAcc.isPrivate()) { + addConnection(entry, parent); + } + } + + ClassEntry outerClass = entry.getOuterClass(); + if (outerClass != null) { + addConnection(entry, outerClass); + } + } + } + + private void addPartitions(EntryIndex entryIndex) { + Set unassignedClasses = Sets.newHashSet(entryIndex.getClasses()); + while (!unassignedClasses.isEmpty()) { + Iterator iterator = unassignedClasses.iterator(); + ClassEntry initialEntry = iterator.next(); + iterator.remove(); + + HashSet partition = Sets.newHashSet(); + partition.add(initialEntry); + buildPartition(unassignedClasses, partition, initialEntry); + partitions.add(partition); + for (ClassEntry entry : partition) { + classPartitions.put(entry, partition); + } + } + } + + public Collection> getPartitions() { + return partitions; + } + + public Set getPartition(ClassEntry classEntry) { + return classPartitions.get(classEntry); + } + + @Override + public void processIndex(JarIndex index) { + EntryIndex entryIndex = index.getEntryIndex(); + ReferenceIndex referenceIndex = index.getReferenceIndex(); + InheritanceIndex inheritanceIndex = index.getInheritanceIndex(); + addConnections(entryIndex, referenceIndex, inheritanceIndex); + addPartitions(entryIndex); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/index/ReferenceIndex.java b/enigma/src/main/java/cuchaz/enigma/analysis/index/ReferenceIndex.java new file mode 100644 index 0000000..b6797c2 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/index/ReferenceIndex.java @@ -0,0 +1,148 @@ +package cuchaz.enigma.analysis.index; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.analysis.ReferenceTargetType; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; +import cuchaz.enigma.translation.representation.Lambda; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.*; + +import java.util.Collection; +import java.util.Map; + +public class ReferenceIndex implements JarIndexer { + private Multimap methodReferences = HashMultimap.create(); + + private Multimap> referencesToMethods = HashMultimap.create(); + private Multimap> referencesToClasses = HashMultimap.create(); + private Multimap> referencesToFields = HashMultimap.create(); + private Multimap> fieldTypeReferences = HashMultimap.create(); + private Multimap> methodTypeReferences = HashMultimap.create(); + + @Override + public void indexMethod(MethodDefEntry methodEntry) { + indexMethodDescriptor(methodEntry, methodEntry.getDesc()); + } + + private void indexMethodDescriptor(MethodDefEntry entry, MethodDescriptor descriptor) { + for (TypeDescriptor typeDescriptor : descriptor.getArgumentDescs()) { + indexMethodTypeDescriptor(entry, typeDescriptor); + } + indexMethodTypeDescriptor(entry, descriptor.getReturnDesc()); + } + + private void indexMethodTypeDescriptor(MethodDefEntry method, TypeDescriptor typeDescriptor) { + if (typeDescriptor.isType()) { + ClassEntry referencedClass = typeDescriptor.getTypeEntry(); + methodTypeReferences.put(referencedClass, new EntryReference<>(referencedClass, referencedClass.getName(), method)); + } else if (typeDescriptor.isArray()) { + indexMethodTypeDescriptor(method, typeDescriptor.getArrayType()); + } + } + + @Override + public void indexField(FieldDefEntry fieldEntry) { + indexFieldTypeDescriptor(fieldEntry, fieldEntry.getDesc()); + } + + private void indexFieldTypeDescriptor(FieldDefEntry field, TypeDescriptor typeDescriptor) { + if (typeDescriptor.isType()) { + ClassEntry referencedClass = typeDescriptor.getTypeEntry(); + fieldTypeReferences.put(referencedClass, new EntryReference<>(referencedClass, referencedClass.getName(), field)); + } else if (typeDescriptor.isArray()) { + indexFieldTypeDescriptor(field, typeDescriptor.getArrayType()); + } + } + + @Override + public void indexMethodReference(MethodDefEntry callerEntry, MethodEntry referencedEntry, ReferenceTargetType targetType) { + referencesToMethods.put(referencedEntry, new EntryReference<>(referencedEntry, referencedEntry.getName(), callerEntry, targetType)); + methodReferences.put(callerEntry, referencedEntry); + + if (referencedEntry.isConstructor()) { + ClassEntry referencedClass = referencedEntry.getParent(); + referencesToClasses.put(referencedClass, new EntryReference<>(referencedClass, referencedEntry.getName(), callerEntry, targetType)); + } + } + + @Override + public void indexFieldReference(MethodDefEntry callerEntry, FieldEntry referencedEntry, ReferenceTargetType targetType) { + referencesToFields.put(referencedEntry, new EntryReference<>(referencedEntry, referencedEntry.getName(), callerEntry, targetType)); + } + + @Override + public void indexLambda(MethodDefEntry callerEntry, Lambda lambda, ReferenceTargetType targetType) { + if (lambda.getImplMethod() instanceof MethodEntry) { + indexMethodReference(callerEntry, (MethodEntry) lambda.getImplMethod(), targetType); + } else { + indexFieldReference(callerEntry, (FieldEntry) lambda.getImplMethod(), targetType); + } + + indexMethodDescriptor(callerEntry, lambda.getInvokedType()); + indexMethodDescriptor(callerEntry, lambda.getSamMethodType()); + indexMethodDescriptor(callerEntry, lambda.getInstantiatedMethodType()); + } + + @Override + public void processIndex(JarIndex index) { + methodReferences = remapReferences(index, methodReferences); + referencesToMethods = remapReferencesTo(index, referencesToMethods); + referencesToClasses = remapReferencesTo(index, referencesToClasses); + referencesToFields = remapReferencesTo(index, referencesToFields); + fieldTypeReferences = remapReferencesTo(index, fieldTypeReferences); + methodTypeReferences = remapReferencesTo(index, methodTypeReferences); + } + + private , V extends Entry> Multimap remapReferences(JarIndex index, Multimap multimap) { + final int keySetSize = multimap.keySet().size(); + Multimap resolved = HashMultimap.create(multimap.keySet().size(), keySetSize == 0 ? 0 : multimap.size() / keySetSize); + for (Map.Entry entry : multimap.entries()) { + resolved.put(remap(index, entry.getKey()), remap(index, entry.getValue())); + } + return resolved; + } + + private , C extends Entry> Multimap> remapReferencesTo(JarIndex index, Multimap> multimap) { + final int keySetSize = multimap.keySet().size(); + Multimap> resolved = HashMultimap.create(keySetSize, keySetSize == 0 ? 0 : multimap.size() / keySetSize); + for (Map.Entry> entry : multimap.entries()) { + resolved.put(remap(index, entry.getKey()), remap(index, entry.getValue())); + } + return resolved; + } + + private > E remap(JarIndex index, E entry) { + return index.getEntryResolver().resolveFirstEntry(entry, ResolutionStrategy.RESOLVE_CLOSEST); + } + + private , C extends Entry> EntryReference remap(JarIndex index, EntryReference reference) { + return index.getEntryResolver().resolveFirstReference(reference, ResolutionStrategy.RESOLVE_CLOSEST); + } + + public Collection getMethodsReferencedBy(MethodEntry entry) { + return methodReferences.get(entry); + } + + public Collection> getReferencesToField(FieldEntry entry) { + return referencesToFields.get(entry); + } + + public Collection> getReferencesToClass(ClassEntry entry) { + return referencesToClasses.get(entry); + } + + public Collection> getReferencesToMethod(MethodEntry entry) { + return referencesToMethods.get(entry); + } + + public Collection> getFieldTypeReferencesToClass(ClassEntry entry) { + return fieldTypeReferences.get(entry); + } + + public Collection> getMethodTypeReferencesToClass(ClassEntry entry) { + return methodTypeReferences.get(entry); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/api/EnigmaPlugin.java b/enigma/src/main/java/cuchaz/enigma/api/EnigmaPlugin.java new file mode 100644 index 0000000..bdd6015 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/api/EnigmaPlugin.java @@ -0,0 +1,5 @@ +package cuchaz.enigma.api; + +public interface EnigmaPlugin { + void init(EnigmaPluginContext ctx); +} diff --git a/enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java b/enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java new file mode 100644 index 0000000..a59051a --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java @@ -0,0 +1,9 @@ +package cuchaz.enigma.api; + +import cuchaz.enigma.api.service.EnigmaService; +import cuchaz.enigma.api.service.EnigmaServiceFactory; +import cuchaz.enigma.api.service.EnigmaServiceType; + +public interface EnigmaPluginContext { + void registerService(String id, EnigmaServiceType serviceType, EnigmaServiceFactory factory); +} diff --git a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaService.java b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaService.java new file mode 100644 index 0000000..526dda7 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaService.java @@ -0,0 +1,4 @@ +package cuchaz.enigma.api.service; + +public interface EnigmaService { +} diff --git a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java new file mode 100644 index 0000000..9e433fb --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java @@ -0,0 +1,11 @@ +package cuchaz.enigma.api.service; + +import java.util.Optional; + +public interface EnigmaServiceContext { + static EnigmaServiceContext empty() { + return key -> Optional.empty(); + } + + Optional getArgument(String key); +} diff --git a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java new file mode 100644 index 0000000..7c10ac2 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java @@ -0,0 +1,5 @@ +package cuchaz.enigma.api.service; + +public interface EnigmaServiceFactory { + T create(EnigmaServiceContext ctx); +} diff --git a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceType.java b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceType.java new file mode 100644 index 0000000..358828f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceType.java @@ -0,0 +1,29 @@ +package cuchaz.enigma.api.service; + +public final class EnigmaServiceType { + public final String key; + + private EnigmaServiceType(String key) { + this.key = key; + } + + public static EnigmaServiceType create(String key) { + return new EnigmaServiceType<>(key); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + + if (obj instanceof EnigmaServiceType) { + return ((EnigmaServiceType) obj).key.equals(key); + } + + return false; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/api/service/JarIndexerService.java b/enigma/src/main/java/cuchaz/enigma/api/service/JarIndexerService.java new file mode 100644 index 0000000..0cda199 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/api/service/JarIndexerService.java @@ -0,0 +1,10 @@ +package cuchaz.enigma.api.service; + +import cuchaz.enigma.analysis.ClassCache; +import cuchaz.enigma.analysis.index.JarIndex; + +public interface JarIndexerService extends EnigmaService { + EnigmaServiceType TYPE = EnigmaServiceType.create("jar_indexer"); + + void acceptJar(ClassCache classCache, JarIndex jarIndex); +} diff --git a/enigma/src/main/java/cuchaz/enigma/api/service/NameProposalService.java b/enigma/src/main/java/cuchaz/enigma/api/service/NameProposalService.java new file mode 100644 index 0000000..4c357db --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/api/service/NameProposalService.java @@ -0,0 +1,12 @@ +package cuchaz.enigma.api.service; + +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.util.Optional; + +public interface NameProposalService extends EnigmaService { + EnigmaServiceType TYPE = EnigmaServiceType.create("name_proposal"); + + Optional proposeName(Entry obfEntry, EntryRemapper remapper); +} diff --git a/enigma/src/main/java/cuchaz/enigma/api/service/ObfuscationTestService.java b/enigma/src/main/java/cuchaz/enigma/api/service/ObfuscationTestService.java new file mode 100644 index 0000000..af0cf30 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/api/service/ObfuscationTestService.java @@ -0,0 +1,9 @@ +package cuchaz.enigma.api.service; + +import cuchaz.enigma.translation.representation.entry.Entry; + +public interface ObfuscationTestService extends EnigmaService { + EnigmaServiceType TYPE = EnigmaServiceType.create("obfuscation_test"); + + boolean testDeobfuscated(Entry entry); +} diff --git a/enigma/src/main/java/cuchaz/enigma/bytecode/translators/AsmObjectTranslator.java b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/AsmObjectTranslator.java new file mode 100644 index 0000000..1a2b47f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/AsmObjectTranslator.java @@ -0,0 +1,46 @@ +package cuchaz.enigma.bytecode.translators; + +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Type; + +public class AsmObjectTranslator { + public static Type translateType(Translator translator, Type type) { + String descString = type.getDescriptor(); + switch (type.getSort()) { + case Type.OBJECT: { + ClassEntry classEntry = new ClassEntry(type.getInternalName()); + return Type.getObjectType(translator.translate(classEntry).getFullName()); + } + case Type.ARRAY: { + TypeDescriptor descriptor = new TypeDescriptor(descString); + return Type.getType(translator.translate(descriptor).toString()); + } + case Type.METHOD: { + MethodDescriptor descriptor = new MethodDescriptor(descString); + return Type.getMethodType(translator.translate(descriptor).toString()); + } + } + return type; + } + + public static Handle translateHandle(Translator translator, Handle handle) { + MethodEntry entry = new MethodEntry(new ClassEntry(handle.getOwner()), handle.getName(), new MethodDescriptor(handle.getDesc())); + MethodEntry translatedMethod = translator.translate(entry); + ClassEntry ownerClass = translatedMethod.getParent(); + return new Handle(handle.getTag(), ownerClass.getFullName(), translatedMethod.getName(), translatedMethod.getDesc().toString(), handle.isInterface()); + } + + public static Object translateValue(Translator translator, Object value) { + if (value instanceof Type) { + return translateType(translator, (Type) value); + } else if (value instanceof Handle) { + return translateHandle(translator, (Handle) value); + } + return value; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/bytecode/translators/LocalVariableFixVisitor.java b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/LocalVariableFixVisitor.java new file mode 100644 index 0000000..cfd8fbe --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/LocalVariableFixVisitor.java @@ -0,0 +1,126 @@ +package cuchaz.enigma.bytecode.translators; + +import com.google.common.base.CharMatcher; +import cuchaz.enigma.translation.LocalNameGenerator; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassDefEntry; +import cuchaz.enigma.translation.representation.entry.MethodDefEntry; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class LocalVariableFixVisitor extends ClassVisitor { + private ClassDefEntry ownerEntry; + + public LocalVariableFixVisitor(int api, ClassVisitor visitor) { + super(api, visitor); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + ownerEntry = ClassDefEntry.parse(access, name, signature, superName, interfaces); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodDefEntry methodEntry = MethodDefEntry.parse(ownerEntry, access, name, descriptor, signature); + return new Method(api, methodEntry, super.visitMethod(access, name, descriptor, signature, exceptions)); + } + + private class Method extends MethodVisitor { + private final MethodDefEntry methodEntry; + private final Map parameterNames = new HashMap<>(); + private final Map parameterIndices = new HashMap<>(); + private boolean hasParameterTable; + private int parameterIndex = 0; + + Method(int api, MethodDefEntry methodEntry, MethodVisitor visitor) { + super(api, visitor); + this.methodEntry = methodEntry; + + int lvIndex = methodEntry.getAccess().isStatic() ? 0 : 1; + List parameters = methodEntry.getDesc().getArgumentDescs(); + for (int parameterIndex = 0; parameterIndex < parameters.size(); parameterIndex++) { + TypeDescriptor param = parameters.get(parameterIndex); + parameterIndices.put(lvIndex, parameterIndex); + lvIndex += param.getSize(); + } + } + + @Override + public void visitParameter(String name, int access) { + hasParameterTable = true; + super.visitParameter(fixParameterName(parameterIndex, name), fixParameterAccess(parameterIndex, access)); + parameterIndex++; + } + + @Override + public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) { + if (index == 0 && !methodEntry.getAccess().isStatic()) { + name = "this"; + } else if (parameterIndices.containsKey(index)) { + name = fixParameterName(parameterIndices.get(index), name); + } else if (isInvalidName(name)) { + name = LocalNameGenerator.generateLocalVariableName(index, new TypeDescriptor(desc)); + } + + super.visitLocalVariable(name, desc, signature, start, end, index); + } + + private boolean isInvalidName(String name) { + return name == null || name.isEmpty() || !CharMatcher.ascii().matchesAllOf(name); + } + + @Override + public void visitEnd() { + if (!hasParameterTable) { + List arguments = methodEntry.getDesc().getArgumentDescs(); + for (int argumentIndex = 0; argumentIndex < arguments.size(); argumentIndex++) { + super.visitParameter(fixParameterName(argumentIndex, null), fixParameterAccess(argumentIndex, 0)); + } + } + + super.visitEnd(); + } + + private String fixParameterName(int index, String name) { + if (parameterNames.get(index) != null) { + return parameterNames.get(index); // to make sure that LVT names are consistent with parameter table names + } + + if (isInvalidName(name)) { + List arguments = methodEntry.getDesc().getArgumentDescs(); + name = LocalNameGenerator.generateArgumentName(index, arguments.get(index), arguments); + } + + if (index == 0 && ownerEntry.getAccess().isEnum() && methodEntry.getName().equals("")) { + name = "name"; + } + + if (index == 1 && ownerEntry.getAccess().isEnum() && methodEntry.getName().equals("")) { + name = "ordinal"; + } + + parameterNames.put(index, name); + return name; + } + + private int fixParameterAccess(int index, int access) { + if (index == 0 && ownerEntry.getAccess().isEnum() && methodEntry.getName().equals("")) { + access |= Opcodes.ACC_SYNTHETIC; + } + + if (index == 1 && ownerEntry.getAccess().isEnum() && methodEntry.getName().equals("")) { + access |= Opcodes.ACC_SYNTHETIC; + } + + return access; + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/bytecode/translators/SourceFixVisitor.java b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/SourceFixVisitor.java new file mode 100644 index 0000000..2b750ea --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/SourceFixVisitor.java @@ -0,0 +1,39 @@ +package cuchaz.enigma.bytecode.translators; + +import cuchaz.enigma.analysis.index.BridgeMethodIndex; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.representation.entry.ClassDefEntry; +import cuchaz.enigma.translation.representation.entry.MethodDefEntry; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class SourceFixVisitor extends ClassVisitor { + private final JarIndex index; + private ClassDefEntry ownerEntry; + + public SourceFixVisitor(int api, ClassVisitor visitor, JarIndex index) { + super(api, visitor); + this.index = index; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + ownerEntry = ClassDefEntry.parse(access, name, signature, superName, interfaces); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodDefEntry methodEntry = MethodDefEntry.parse(ownerEntry, access, name, descriptor, signature); + + BridgeMethodIndex bridgeIndex = index.getBridgeMethodIndex(); + if (bridgeIndex.isBridgeMethod(methodEntry)) { + access |= Opcodes.ACC_BRIDGE; + } else if (bridgeIndex.isSpecializedMethod(methodEntry)) { + name = bridgeIndex.getBridgeFromSpecialized(methodEntry).getName(); + } + + return super.visitMethod(access, name, descriptor, signature, exceptions); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationAnnotationVisitor.java b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationAnnotationVisitor.java new file mode 100644 index 0000000..cb843ad --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationAnnotationVisitor.java @@ -0,0 +1,51 @@ +package cuchaz.enigma.bytecode.translators; + +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import org.objectweb.asm.AnnotationVisitor; + +public class TranslationAnnotationVisitor extends AnnotationVisitor { + private final Translator translator; + private final ClassEntry annotationEntry; + + public TranslationAnnotationVisitor(Translator translator, ClassEntry annotationEntry, int api, AnnotationVisitor av) { + super(api, av); + this.translator = translator; + this.annotationEntry = annotationEntry; + } + + @Override + public void visit(String name, Object value) { + super.visit(name, AsmObjectTranslator.translateValue(translator, value)); + } + + @Override + public AnnotationVisitor visitArray(String name) { + return new TranslationAnnotationVisitor(translator, annotationEntry, api, super.visitArray(name)); + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String desc) { + TypeDescriptor type = new TypeDescriptor(desc); + if (name != null) { + FieldEntry annotationField = translator.translate(new FieldEntry(annotationEntry, name, type)); + return super.visitAnnotation(annotationField.getName(), annotationField.getDesc().toString()); + } else { + return super.visitAnnotation(null, translator.translate(type).toString()); + } + } + + @Override + public void visitEnum(String name, String desc, String value) { + TypeDescriptor type = new TypeDescriptor(desc); + FieldEntry enumField = translator.translate(new FieldEntry(type.getTypeEntry(), value, type)); + if (name != null) { + FieldEntry annotationField = translator.translate(new FieldEntry(annotationEntry, name, type)); + super.visitEnum(annotationField.getName(), annotationField.getDesc().toString(), enumField.getName()); + } else { + super.visitEnum(null, translator.translate(type).toString(), enumField.getName()); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationClassVisitor.java b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationClassVisitor.java new file mode 100644 index 0000000..e4c41d3 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationClassVisitor.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.bytecode.translators; + +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.*; +import org.objectweb.asm.*; + +import java.util.Arrays; + +public class TranslationClassVisitor extends ClassVisitor { + private final Translator translator; + + private ClassDefEntry obfClassEntry; + + public TranslationClassVisitor(Translator translator, int api, ClassVisitor cv) { + super(api, cv); + this.translator = translator; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + obfClassEntry = ClassDefEntry.parse(access, name, signature, superName, interfaces); + + ClassDefEntry translatedEntry = translator.translate(obfClassEntry); + String translatedSuper = translatedEntry.getSuperClass() != null ? translatedEntry.getSuperClass().getFullName() : null; + String[] translatedInterfaces = Arrays.stream(translatedEntry.getInterfaces()).map(ClassEntry::getFullName).toArray(String[]::new); + + super.visit(version, translatedEntry.getAccess().getFlags(), translatedEntry.getFullName(), translatedEntry.getSignature().toString(), translatedSuper, translatedInterfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + FieldDefEntry entry = FieldDefEntry.parse(obfClassEntry, access, name, desc, signature); + FieldDefEntry translatedEntry = translator.translate(entry); + FieldVisitor fv = super.visitField(translatedEntry.getAccess().getFlags(), translatedEntry.getName(), translatedEntry.getDesc().toString(), translatedEntry.getSignature().toString(), value); + return new TranslationFieldVisitor(translator, translatedEntry, api, fv); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + MethodDefEntry entry = MethodDefEntry.parse(obfClassEntry, access, name, desc, signature); + MethodDefEntry translatedEntry = translator.translate(entry); + String[] translatedExceptions = new String[exceptions.length]; + for (int i = 0; i < exceptions.length; i++) { + translatedExceptions[i] = translator.translate(new ClassEntry(exceptions[i])).getFullName(); + } + MethodVisitor mv = super.visitMethod(translatedEntry.getAccess().getFlags(), translatedEntry.getName(), translatedEntry.getDesc().toString(), translatedEntry.getSignature().toString(), translatedExceptions); + return new TranslationMethodVisitor(translator, obfClassEntry, entry, api, mv); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + ClassDefEntry classEntry = ClassDefEntry.parse(access, name, obfClassEntry.getSignature().toString(), null, new String[0]); + ClassDefEntry translatedEntry = translator.translate(classEntry); + ClassEntry translatedOuterClass = translatedEntry.getOuterClass(); + if (translatedOuterClass == null) { + throw new IllegalStateException("Translated inner class did not have outer class"); + } + + // Anonymous classes do not specify an outer or inner name. As we do not translate from the given parameter, ignore if the input is null + String translatedName = translatedEntry.getFullName(); + String translatedOuterName = outerName != null ? translatedOuterClass.getFullName() : null; + String translatedInnerName = innerName != null ? translatedEntry.getName() : null; + super.visitInnerClass(translatedName, translatedOuterName, translatedInnerName, translatedEntry.getAccess().getFlags()); + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + if (desc != null) { + MethodEntry translatedEntry = translator.translate(new MethodEntry(new ClassEntry(owner), name, new MethodDescriptor(desc))); + super.visitOuterClass(translatedEntry.getParent().getFullName(), translatedEntry.getName(), translatedEntry.getDesc().toString()); + } else { + super.visitOuterClass(owner, name, desc); + } + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + TypeDescriptor translatedDesc = translator.translate(new TypeDescriptor(desc)); + AnnotationVisitor av = super.visitAnnotation(translatedDesc.toString(), visible); + return new TranslationAnnotationVisitor(translator, translatedDesc.getTypeEntry(), api, av); + } + + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) { + TypeDescriptor translatedDesc = translator.translate(new TypeDescriptor(desc)); + AnnotationVisitor av = super.visitTypeAnnotation(typeRef, typePath, translatedDesc.toString(), visible); + return new TranslationAnnotationVisitor(translator, translatedDesc.getTypeEntry(), api, av); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationFieldVisitor.java b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationFieldVisitor.java new file mode 100644 index 0000000..28fc199 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationFieldVisitor.java @@ -0,0 +1,33 @@ +package cuchaz.enigma.bytecode.translators; + +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.FieldDefEntry; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.TypePath; + +public class TranslationFieldVisitor extends FieldVisitor { + private final FieldDefEntry fieldEntry; + private final Translator translator; + + public TranslationFieldVisitor(Translator translator, FieldDefEntry fieldEntry, int api, FieldVisitor fv) { + super(api, fv); + this.translator = translator; + this.fieldEntry = fieldEntry; + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + TypeDescriptor typeDesc = translator.translate(new TypeDescriptor(desc)); + AnnotationVisitor av = super.visitAnnotation(typeDesc.toString(), visible); + return new TranslationAnnotationVisitor(translator, typeDesc.getTypeEntry(), api, av); + } + + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) { + TypeDescriptor typeDesc = translator.translate(new TypeDescriptor(desc)); + AnnotationVisitor av = super.visitAnnotation(typeDesc.toString(), visible); + return new TranslationAnnotationVisitor(translator, typeDesc.getTypeEntry(), api, av); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationMethodVisitor.java b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationMethodVisitor.java new file mode 100644 index 0000000..a82df1b --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationMethodVisitor.java @@ -0,0 +1,145 @@ +package cuchaz.enigma.bytecode.translators; + +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.Signature; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.*; +import org.objectweb.asm.*; + +public class TranslationMethodVisitor extends MethodVisitor { + private final MethodDefEntry methodEntry; + private final Translator translator; + + private int parameterIndex = 0; + private int parameterLvIndex; + + public TranslationMethodVisitor(Translator translator, ClassDefEntry ownerEntry, MethodDefEntry methodEntry, int api, MethodVisitor mv) { + super(api, mv); + this.translator = translator; + this.methodEntry = methodEntry; + + parameterLvIndex = methodEntry.getAccess().isStatic() ? 0 : 1; + } + + @Override + public void visitParameter(String name, int access) { + name = translateVariableName(parameterLvIndex, name); + parameterLvIndex += methodEntry.getDesc().getArgumentDescs().get(parameterIndex++).getSize(); + + super.visitParameter(name, access); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + FieldEntry entry = new FieldEntry(new ClassEntry(owner), name, new TypeDescriptor(desc)); + FieldEntry translatedEntry = translator.translate(entry); + super.visitFieldInsn(opcode, translatedEntry.getParent().getFullName(), translatedEntry.getName(), translatedEntry.getDesc().toString()); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + MethodEntry entry = new MethodEntry(new ClassEntry(owner), name, new MethodDescriptor(desc)); + MethodEntry translatedEntry = translator.translate(entry); + super.visitMethodInsn(opcode, translatedEntry.getParent().getFullName(), translatedEntry.getName(), translatedEntry.getDesc().toString(), itf); + } + + @Override + public void visitFrame(int type, int localCount, Object[] locals, int stackCount, Object[] stack) { + Object[] translatedLocals = this.getTranslatedFrame(locals, localCount); + Object[] translatedStack = this.getTranslatedFrame(stack, stackCount); + super.visitFrame(type, localCount, translatedLocals, stackCount, translatedStack); + } + + private Object[] getTranslatedFrame(Object[] array, int count) { + if (array == null) { + return null; + } + for (int i = 0; i < count; i++) { + Object object = array[i]; + if (object instanceof String) { + String type = (String) object; + array[i] = translator.translate(new ClassEntry(type)).getFullName(); + } + } + return array; + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + TypeDescriptor typeDesc = translator.translate(new TypeDescriptor(desc)); + AnnotationVisitor av = super.visitAnnotation(typeDesc.toString(), visible); + return new TranslationAnnotationVisitor(translator, typeDesc.getTypeEntry(), api, av); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + TypeDescriptor typeDesc = translator.translate(new TypeDescriptor(desc)); + AnnotationVisitor av = super.visitParameterAnnotation(parameter, typeDesc.toString(), visible); + return new TranslationAnnotationVisitor(translator, typeDesc.getTypeEntry(), api, av); + } + + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) { + TypeDescriptor typeDesc = translator.translate(new TypeDescriptor(desc)); + AnnotationVisitor av = super.visitTypeAnnotation(typeRef, typePath, typeDesc.toString(), visible); + return new TranslationAnnotationVisitor(translator, typeDesc.getTypeEntry(), api, av); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + ClassEntry translatedEntry = translator.translate(new ClassEntry(type)); + super.visitTypeInsn(opcode, translatedEntry.getFullName()); + } + + @Override + public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) { + MethodDescriptor translatedMethodDesc = translator.translate(new MethodDescriptor(desc)); + Object[] translatedBsmArgs = new Object[bsmArgs.length]; + for (int i = 0; i < bsmArgs.length; i++) { + translatedBsmArgs[i] = AsmObjectTranslator.translateValue(translator, bsmArgs[i]); + } + super.visitInvokeDynamicInsn(name, translatedMethodDesc.toString(), AsmObjectTranslator.translateHandle(translator, bsm), translatedBsmArgs); + } + + @Override + public void visitLdcInsn(Object cst) { + super.visitLdcInsn(AsmObjectTranslator.translateValue(translator, cst)); + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + super.visitMultiANewArrayInsn(translator.translate(new TypeDescriptor(desc)).toString(), dims); + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (type != null) { + ClassEntry translatedEntry = translator.translate(new ClassEntry(type)); + super.visitTryCatchBlock(start, end, handler, translatedEntry.getFullName()); + } else { + super.visitTryCatchBlock(start, end, handler, type); + } + } + + @Override + public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) { + signature = translator.translate(Signature.createTypedSignature(signature)).toString(); + name = translateVariableName(index, name); + desc = translator.translate(new TypeDescriptor(desc)).toString(); + + super.visitLocalVariable(name, desc, signature, start, end, index); + } + + private String translateVariableName(int index, String name) { + LocalVariableEntry entry = new LocalVariableEntry(methodEntry, index, "", true,null); + LocalVariableEntry translatedEntry = translator.translate(entry); + String translatedName = translatedEntry.getName(); + + if (!translatedName.isEmpty()) { + return translatedName; + } + + return name; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationSignatureVisitor.java b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationSignatureVisitor.java new file mode 100644 index 0000000..bc4a2f0 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/bytecode/translators/TranslationSignatureVisitor.java @@ -0,0 +1,129 @@ +package cuchaz.enigma.bytecode.translators; + +import cuchaz.enigma.Enigma; +import org.objectweb.asm.signature.SignatureVisitor; + +import java.util.Stack; +import java.util.function.Function; + +public class TranslationSignatureVisitor extends SignatureVisitor { + private final Function remapper; + + private final SignatureVisitor sv; + private final Stack classStack = new Stack<>(); + + public TranslationSignatureVisitor(Function remapper, SignatureVisitor sv) { + super(Enigma.ASM_VERSION); + this.remapper = remapper; + this.sv = sv; + } + + @Override + public void visitClassType(String name) { + classStack.push(name); + String translatedEntry = this.remapper.apply(name); + this.sv.visitClassType(translatedEntry); + } + + @Override + public void visitInnerClassType(String name) { + String lastClass = classStack.pop(); + if (!name.startsWith(lastClass+"$")){//todo see if there's a way to base this on whether there were type params or not + name = lastClass+"$"+name; + } + String translatedEntry = this.remapper.apply(name); + if (translatedEntry.contains("/")){ + translatedEntry = translatedEntry.substring(translatedEntry.lastIndexOf("/")+1); + } + if (translatedEntry.contains("$")){ + translatedEntry = translatedEntry.substring(translatedEntry.lastIndexOf("$")+1); + } + this.sv.visitInnerClassType(translatedEntry); + } + + @Override + public void visitFormalTypeParameter(String name) { + this.sv.visitFormalTypeParameter(name); + } + + @Override + public void visitTypeVariable(String name) { + this.sv.visitTypeVariable(name); + } + + @Override + public SignatureVisitor visitArrayType() { + this.sv.visitArrayType(); + return this; + } + + @Override + public void visitBaseType(char descriptor) { + this.sv.visitBaseType(descriptor); + } + + @Override + public SignatureVisitor visitClassBound() { + this.sv.visitClassBound(); + return this; + } + + @Override + public SignatureVisitor visitExceptionType() { + this.sv.visitExceptionType(); + return this; + } + + @Override + public SignatureVisitor visitInterface() { + this.sv.visitInterface(); + return this; + } + + @Override + public SignatureVisitor visitInterfaceBound() { + this.sv.visitInterfaceBound(); + return this; + } + + @Override + public SignatureVisitor visitParameterType() { + this.sv.visitParameterType(); + return this; + } + + @Override + public SignatureVisitor visitReturnType() { + this.sv.visitReturnType(); + return this; + } + + @Override + public SignatureVisitor visitSuperclass() { + this.sv.visitSuperclass(); + return this; + } + + @Override + public void visitTypeArgument() { + this.sv.visitTypeArgument(); + } + + @Override + public SignatureVisitor visitTypeArgument(char wildcard) { + this.sv.visitTypeArgument(wildcard); + return this; + } + + @Override + public void visitEnd() { + this.sv.visitEnd(); + if (!classStack.empty()) + classStack.pop(); + } + + @Override + public String toString() { + return this.sv.toString(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/Decompiler.java b/enigma/src/main/java/cuchaz/enigma/source/Decompiler.java new file mode 100644 index 0000000..c9666d5 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/Decompiler.java @@ -0,0 +1,5 @@ +package cuchaz.enigma.source; + +public interface Decompiler { + Source getSource(String className); +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/DecompilerService.java b/enigma/src/main/java/cuchaz/enigma/source/DecompilerService.java new file mode 100644 index 0000000..377ccbc --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/DecompilerService.java @@ -0,0 +1,11 @@ +package cuchaz.enigma.source; + +import cuchaz.enigma.ClassProvider; +import cuchaz.enigma.api.service.EnigmaService; +import cuchaz.enigma.api.service.EnigmaServiceType; + +public interface DecompilerService extends EnigmaService { + EnigmaServiceType TYPE = EnigmaServiceType.create("decompiler"); + + Decompiler create(ClassProvider classProvider, SourceSettings settings); +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/Decompilers.java b/enigma/src/main/java/cuchaz/enigma/source/Decompilers.java new file mode 100644 index 0000000..7d154a6 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/Decompilers.java @@ -0,0 +1,9 @@ +package cuchaz.enigma.source; + +import cuchaz.enigma.source.cfr.CfrDecompiler; +import cuchaz.enigma.source.procyon.ProcyonDecompiler; + +public class Decompilers { + public static final DecompilerService PROCYON = ProcyonDecompiler::new; + public static final DecompilerService CFR = CfrDecompiler::new; +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/Source.java b/enigma/src/main/java/cuchaz/enigma/source/Source.java new file mode 100644 index 0000000..43c4de0 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/Source.java @@ -0,0 +1,11 @@ +package cuchaz.enigma.source; + +import cuchaz.enigma.translation.mapping.EntryRemapper; + +public interface Source { + String asString(); + + Source addJavadocs(EntryRemapper remapper); + + SourceIndex index(); +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/SourceIndex.java b/enigma/src/main/java/cuchaz/enigma/source/SourceIndex.java new file mode 100644 index 0000000..178ce20 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/SourceIndex.java @@ -0,0 +1,172 @@ +package cuchaz.enigma.source; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +public class SourceIndex { + private String source; + private List lineOffsets; + private final TreeMap, Entry>> tokenToReference; + private final Multimap, Entry>, Token> referenceToTokens; + private final Map, Token> declarationToToken; + + public SourceIndex() { + tokenToReference = new TreeMap<>(); + referenceToTokens = HashMultimap.create(); + declarationToToken = Maps.newHashMap(); + } + + public SourceIndex(String source) { + this(); + setSource(source); + } + + public void setSource(String source) { + this.source = source; + lineOffsets = Lists.newArrayList(); + lineOffsets.add(0); + + for (int i = 0; i < this.source.length(); i++) { + if (this.source.charAt(i) == '\n') { + lineOffsets.add(i + 1); + } + } + } + + public String getSource() { + return source; + } + + public int getLineNumber(int position) { + int line = 0; + + for (int offset : lineOffsets) { + if (offset > position) { + break; + } + + line++; + } + + return line; + } + + public int getColumnNumber(int position) { + return position - lineOffsets.get(getLineNumber(position) - 1) + 1; + } + + public int getPosition(int line, int column) { + return lineOffsets.get(line - 1) + column - 1; + } + + public Iterable> declarations() { + return declarationToToken.keySet(); + } + + public Iterable declarationTokens() { + return declarationToToken.values(); + } + + public Token getDeclarationToken(Entry entry) { + return declarationToToken.get(entry); + } + + public void addDeclaration(Token token, Entry deobfEntry) { + if (token != null) { + EntryReference, Entry> reference = new EntryReference<>(deobfEntry, token.text); + tokenToReference.put(token, reference); + referenceToTokens.put(reference, token); + declarationToToken.put(deobfEntry, token); + } + } + + public Iterable, Entry>> references() { + return referenceToTokens.keySet(); + } + + public EntryReference, Entry> getReference(Token token) { + if (token == null) { + return null; + } + + return tokenToReference.get(token); + } + + public Iterable referenceTokens() { + return tokenToReference.keySet(); + } + + public Token getReferenceToken(int pos) { + Token token = tokenToReference.floorKey(new Token(pos, pos, null)); + + if (token != null && token.contains(pos)) { + return token; + } + + return null; + } + + public Collection getReferenceTokens(EntryReference, Entry> deobfReference) { + return referenceToTokens.get(deobfReference); + } + + public void addReference(Token token, Entry deobfEntry, Entry deobfContext) { + if (token != null) { + EntryReference, Entry> deobfReference = new EntryReference<>(deobfEntry, token.text, deobfContext); + tokenToReference.put(token, deobfReference); + referenceToTokens.put(deobfReference, token); + } + } + + public void resolveReferences(EntryResolver resolver) { + // resolve all the classes in the source references + for (Token token : Lists.newArrayList(referenceToTokens.values())) { + EntryReference, Entry> reference = tokenToReference.get(token); + EntryReference, Entry> resolvedReference = resolver.resolveFirstReference(reference, ResolutionStrategy.RESOLVE_CLOSEST); + + // replace the reference + tokenToReference.replace(token, resolvedReference); + + Collection tokens = referenceToTokens.removeAll(reference); + referenceToTokens.putAll(resolvedReference, tokens); + } + } + + public SourceIndex remapTo(SourceRemapper.Result result) { + SourceIndex remapped = new SourceIndex(result.getSource()); + + for (Map.Entry, Token> entry : declarationToToken.entrySet()) { + remapped.declarationToToken.put(entry.getKey(), result.getRemappedToken(entry.getValue())); + } + + for (Map.Entry, Entry>, Collection> entry : referenceToTokens.asMap().entrySet()) { + EntryReference, Entry> reference = entry.getKey(); + Collection oldTokens = entry.getValue(); + + Collection newTokens = oldTokens + .stream() + .map(result::getRemappedToken) + .collect(Collectors.toList()); + + remapped.referenceToTokens.putAll(reference, newTokens); + } + + for (Map.Entry, Entry>> entry : tokenToReference.entrySet()) { + remapped.tokenToReference.put(result.getRemappedToken(entry.getKey()), entry.getValue()); + } + + return remapped; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/SourceRemapper.java b/enigma/src/main/java/cuchaz/enigma/source/SourceRemapper.java new file mode 100644 index 0000000..b5f8006 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/SourceRemapper.java @@ -0,0 +1,62 @@ +package cuchaz.enigma.source; + +import java.util.HashMap; +import java.util.Map; + +public class SourceRemapper { + private final String source; + private final Iterable tokens; + + public SourceRemapper(String source, Iterable tokens) { + this.source = source; + this.tokens = tokens; + } + + public Result remap(Remapper remapper) { + StringBuffer remappedSource = new StringBuffer(source); + Map remappedTokens = new HashMap<>(); + + int accumulatedOffset = 0; + for (Token token : tokens) { + Token movedToken = token.move(accumulatedOffset); + + String remappedName = remapper.remap(token, movedToken); + if (remappedName != null) { + accumulatedOffset += movedToken.getRenameOffset(remappedName); + movedToken.rename(remappedSource, remappedName); + } + + if (!token.equals(movedToken)) { + remappedTokens.put(token, movedToken); + } + } + + return new Result(remappedSource.toString(), remappedTokens); + } + + public static class Result { + private final String remappedSource; + private final Map remappedTokens; + + Result(String remappedSource, Map remappedTokens) { + this.remappedSource = remappedSource; + this.remappedTokens = remappedTokens; + } + + public String getSource() { + return remappedSource; + } + + public Token getRemappedToken(Token token) { + return remappedTokens.getOrDefault(token, token); + } + + public boolean isEmpty() { + return remappedTokens.isEmpty(); + } + } + + public interface Remapper { + String remap(Token token, Token movedToken); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/SourceSettings.java b/enigma/src/main/java/cuchaz/enigma/source/SourceSettings.java new file mode 100644 index 0000000..f6c68e9 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/SourceSettings.java @@ -0,0 +1,11 @@ +package cuchaz.enigma.source; + +public class SourceSettings { + public final boolean removeImports; + public final boolean removeVariableFinal; + + public SourceSettings(boolean removeImports, boolean removeVariableFinal) { + this.removeImports = removeImports; + this.removeVariableFinal = removeVariableFinal; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/Token.java b/enigma/src/main/java/cuchaz/enigma/source/Token.java new file mode 100644 index 0000000..fad733f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/Token.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.source; + +public class Token implements Comparable { + + public int start; + public int end; + public String text; + + public Token(int start, int end, String text) { + this.start = start; + this.end = end; + this.text = text; + } + + public int getRenameOffset(String to) { + int length = this.end - this.start; + return to.length() - length; + } + + public void rename(StringBuffer source, String to) { + int oldEnd = this.end; + this.text = to; + this.end = this.start + to.length(); + + source.replace(start, oldEnd, to); + } + + public Token move(int offset) { + Token token = new Token(this.start + offset, this.end + offset, null); + token.text = text; + return token; + } + + public boolean contains(int pos) { + return pos >= start && pos <= end; + } + + @Override + public int compareTo(Token other) { + return start - other.start; + } + + @Override + public boolean equals(Object other) { + return other instanceof Token && equals((Token) other); + } + + @Override + public int hashCode() { + return start * 37 + end; + } + + public boolean equals(Token other) { + return start == other.start && end == other.end && text.equals(other.text); + } + + @Override + public String toString() { + return String.format("[%d,%d]", start, end); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDecompiler.java b/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDecompiler.java new file mode 100644 index 0000000..9e37f16 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDecompiler.java @@ -0,0 +1,108 @@ +package cuchaz.enigma.source.cfr; + +import com.google.common.io.ByteStreams; +import cuchaz.enigma.ClassProvider; +import cuchaz.enigma.source.Decompiler; +import cuchaz.enigma.source.Source; +import cuchaz.enigma.source.SourceSettings; +import org.benf.cfr.reader.apiunreleased.ClassFileSource2; +import org.benf.cfr.reader.apiunreleased.JarContent; +import org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair; +import org.benf.cfr.reader.entities.ClassFile; +import org.benf.cfr.reader.mapping.MappingFactory; +import org.benf.cfr.reader.mapping.ObfuscationMapping; +import org.benf.cfr.reader.relationship.MemberNameResolver; +import org.benf.cfr.reader.state.DCCommonState; +import org.benf.cfr.reader.state.TypeUsageCollectingDumper; +import org.benf.cfr.reader.util.AnalysisType; +import org.benf.cfr.reader.util.CannotLoadClassException; +import org.benf.cfr.reader.util.collections.ListFactory; +import org.benf.cfr.reader.util.getopt.Options; +import org.benf.cfr.reader.util.getopt.OptionsImpl; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.ClassNode; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + + +public class CfrDecompiler implements Decompiler { + private final DCCommonState state; + + public CfrDecompiler(ClassProvider classProvider, SourceSettings sourceSettings) { + Map options = new HashMap<>(); + + state = new DCCommonState(OptionsImpl.getFactory().create(options), new ClassFileSource2() { + @Override + public JarContent addJarContent(String s, AnalysisType analysisType) { + return null; + } + + @Override + public void informAnalysisRelativePathDetail(String usePath, String classFilePath) { + + } + + @Override + public Collection addJar(String jarPath) { + return null; + } + + @Override + public String getPossiblyRenamedPath(String path) { + return path; + } + + @Override + public Pair getClassFileContent(String path) { + ClassNode node = classProvider.getClassNode(path.substring(0, path.lastIndexOf('.'))); + + if (node == null) { + try (InputStream classResource = CfrDecompiler.class.getClassLoader().getResourceAsStream(path)) { + if (classResource != null) { + return new Pair<>(ByteStreams.toByteArray(classResource), path); + } + } catch (IOException ignored) {} + + return null; + } + + ClassWriter cw = new ClassWriter(0); + node.accept(cw); + return new Pair<>(cw.toByteArray(), path); + } + }); + } + + @Override + public Source getSource(String className) { + DCCommonState state = this.state; + Options options = state.getOptions(); + + ObfuscationMapping mapping = MappingFactory.get(options, state); + state = new DCCommonState(state, mapping); + ClassFile tree = state.getClassFileMaybePath(className); + + state.configureWith(tree); + + // To make sure we're analysing the cached version + try { + tree = state.getClassFile(tree.getClassType()); + } catch (CannotLoadClassException ignored) {} + + if (options.getOption(OptionsImpl.DECOMPILE_INNER_CLASSES)) { + tree.loadInnerClasses(state); + } + + if (options.getOption(OptionsImpl.RENAME_DUP_MEMBERS)) { + MemberNameResolver.resolveNames(state, ListFactory.newList(state.getClassCache().getLoadedTypes())); + } + + TypeUsageCollectingDumper typeUsageCollector = new TypeUsageCollectingDumper(options, tree); + tree.analyseTop(state, typeUsageCollector); + return new CfrSource(tree, state, typeUsageCollector.getRealTypeUsageInformation()); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrSource.java b/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrSource.java new file mode 100644 index 0000000..d4f2da6 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrSource.java @@ -0,0 +1,38 @@ +package cuchaz.enigma.source.cfr; + +import cuchaz.enigma.source.Source; +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import org.benf.cfr.reader.entities.ClassFile; +import org.benf.cfr.reader.state.DCCommonState; +import org.benf.cfr.reader.state.TypeUsageInformation; + +public class CfrSource implements Source { + private final ClassFile tree; + private final SourceIndex index; + private final String string; + + public CfrSource(ClassFile tree, DCCommonState state, TypeUsageInformation typeUsages) { + this.tree = tree; + + EnigmaDumper dumper = new EnigmaDumper(typeUsages); + tree.dump(state.getObfuscationMapping().wrap(dumper)); + index = dumper.getIndex(); + string = dumper.getString(); + } + + @Override + public String asString() { + return string; + } + + @Override + public Source addJavadocs(EntryRemapper remapper) { + return this; // TODO + } + + @Override + public SourceIndex index() { + return index; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/cfr/EnigmaDumper.java b/enigma/src/main/java/cuchaz/enigma/source/cfr/EnigmaDumper.java new file mode 100644 index 0000000..e05a6ff --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/cfr/EnigmaDumper.java @@ -0,0 +1,433 @@ +package cuchaz.enigma.source.cfr; + +import cuchaz.enigma.source.Token; +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.*; +import org.benf.cfr.reader.bytecode.analysis.types.*; +import org.benf.cfr.reader.bytecode.analysis.variables.NamedVariable; +import org.benf.cfr.reader.entities.Field; +import org.benf.cfr.reader.entities.Method; +import org.benf.cfr.reader.mapping.NullMapping; +import org.benf.cfr.reader.mapping.ObfuscationMapping; +import org.benf.cfr.reader.state.TypeUsageInformation; +import org.benf.cfr.reader.util.collections.SetFactory; +import org.benf.cfr.reader.util.output.DelegatingDumper; +import org.benf.cfr.reader.util.output.Dumpable; +import org.benf.cfr.reader.util.output.Dumper; +import org.benf.cfr.reader.util.output.TypeContext; + +import java.util.Set; +import java.util.stream.Collectors; + +public class EnigmaDumper implements Dumper { + private int outputCount = 0; + private int indent; + private boolean atStart = true; + private boolean pendingCR = false; + private final StringBuilder sb = new StringBuilder(); + private final TypeUsageInformation typeUsageInformation; + private final Set emitted = SetFactory.newSet(); + private final SourceIndex index = new SourceIndex(); + private int position; + + + public EnigmaDumper(TypeUsageInformation typeUsageInformation) { + this.typeUsageInformation = typeUsageInformation; + } + + private void append(String s) { + sb.append(s); + position += s.length(); + } + + private String getDesc(JavaTypeInstance type) { + if (!type.isUsableType() && type != RawJavaType.VOID) { + throw new IllegalArgumentException(type.toString()); + } + + if (type instanceof JavaGenericBaseInstance) { + return getDesc(type.getDeGenerifiedType()); + } + + if (type instanceof JavaRefTypeInstance) { + return "L" + type.getRawName().replace('.', '/') + ";"; + } + + if (type instanceof JavaArrayTypeInstance) { + return "[" + getDesc(((JavaArrayTypeInstance) type).removeAnArrayIndirection()); + } + + if (type instanceof RawJavaType) { + switch ((RawJavaType) type) { + case BOOLEAN: + return "Z"; + case BYTE: + return "B"; + case CHAR: + return "C"; + case SHORT: + return "S"; + case INT: + return "I"; + case LONG: + return "J"; + case FLOAT: + return "F"; + case DOUBLE: + return "D"; + case VOID: + return "V"; + default: + throw new AssertionError(); + } + } + + throw new AssertionError(); + } + + private MethodEntry getMethodEntry(MethodPrototype method) { + if (method == null || method.getClassType() == null) { + return null; + } + + MethodDescriptor desc = new MethodDescriptor( + method.getArgs().stream().map(type -> new TypeDescriptor(getDesc(type))).collect(Collectors.toList()), + new TypeDescriptor(method.getName().equals("") || method.getName().equals("") ? "V" : getDesc(method.getReturnType())) + ); + + return new MethodEntry(getClassEntry(method.getClassType()), method.getName(), desc); + } + + private LocalVariableEntry getParameterEntry(MethodPrototype method, int parameterIndex, String name) { + int variableIndex = method.isInstanceMethod() ? 1 : 0; + for (int i = 0; i < parameterIndex; i++) { + variableIndex += method.getArgs().get(i).getStackType().getComputationCategory(); + } + + return new LocalVariableEntry(getMethodEntry(method), variableIndex, name, true, null); + } + + private FieldEntry getFieldEntry(JavaTypeInstance owner, String name, JavaTypeInstance type) { + return new FieldEntry(getClassEntry(owner), name, new TypeDescriptor(getDesc(type))); + } + + private ClassEntry getClassEntry(JavaTypeInstance type) { + return new ClassEntry(type.getRawName().replace('.', '/')); + } + + @Override + public Dumper beginBlockComment(boolean inline) { + print("/*").newln(); + return this; + } + + @Override + public Dumper endBlockComment() { + print(" */").newln(); + return this; + } + + @Override + public Dumper label(String s, boolean inline) { + processPendingCR(); + append(s); + append(":"); + return this; + } + + @Override + public Dumper comment(String s) { + append("// "); + append(s); + append("\n"); + return this; + } + + @Override + public void enqueuePendingCarriageReturn() { + pendingCR = true; + } + + @Override + public Dumper removePendingCarriageReturn() { + pendingCR = false; + return this; + } + + private void processPendingCR() { + if (pendingCR) { + append("\n"); + atStart = true; + pendingCR = false; + } + } + + @Override + public Dumper identifier(String s, Object ref, boolean defines) { + return print(s); + } + + @Override + public Dumper methodName(String name, MethodPrototype method, boolean special, boolean defines) { + doIndent(); + Token token = new Token(position, position + name.length(), name); + Entry entry = getMethodEntry(method); + + if (entry != null) { + if (defines) { + index.addDeclaration(token, entry); + } else { + index.addReference(token, entry, null); + } + } + + return identifier(name, null, defines); + } + + @Override + public Dumper parameterName(String name, MethodPrototype method, int index, boolean defines) { + doIndent(); + Token token = new Token(position, position + name.length(), name); + Entry entry = getParameterEntry(method, index, name); + + if (entry != null) { + if (defines) { + this.index.addDeclaration(token, entry); + } else { + this.index.addReference(token, entry, null); + } + } + + return identifier(name, null, defines); + } + + @Override + public Dumper variableName(String name, NamedVariable variable, boolean defines) { + return identifier(name, null, defines); + } + + @Override + public Dumper packageName(JavaRefTypeInstance t) { + String s = t.getPackageName(); + + if (!s.isEmpty()) { + keyword("package ").print(s).endCodeln().newln(); + } + + return this; + } + + @Override + public Dumper fieldName(String name, Field field, JavaTypeInstance owner, boolean hiddenDeclaration, boolean defines) { + doIndent(); + Token token = new Token(position, position + name.length(), name); + Entry entry = field == null ? null : getFieldEntry(owner, name, field.getJavaTypeInstance()); + + if (entry != null) { + if (defines) { + index.addDeclaration(token, entry); + } else { + index.addReference(token, entry, null); + } + } + + identifier(name, null, defines); + return this; + } + + @Override + public Dumper print(String s) { + processPendingCR(); + doIndent(); + append(s); + atStart = s.endsWith("\n"); + outputCount++; + return this; + } + + @Override + public Dumper print(char c) { + return print(String.valueOf(c)); + } + + @Override + public Dumper newln() { + append("\n"); + atStart = true; + outputCount++; + return this; + } + + @Override + public Dumper endCodeln() { + append(";\n"); + atStart = true; + outputCount++; + return this; + } + + @Override + public Dumper keyword(String s) { + print(s); + return this; + } + + @Override + public Dumper operator(String s) { + print(s); + return this; + } + + @Override + public Dumper separator(String s) { + print(s); + return this; + } + + @Override + public Dumper literal(String s, Object o) { + print(s); + return this; + } + + private void doIndent() { + if (!atStart) return; + String indents = " "; + + for (int x = 0; x < indent; ++x) { + append(indents); + } + + atStart = false; + } + + @Override + public void indent(int diff) { + indent += diff; + } + + @Override + public Dumper dump(Dumpable d) { + if (d == null) { + keyword("null"); + return this; + } + + d.dump(this); + return this; + } + + @Override + public TypeUsageInformation getTypeUsageInformation() { + return typeUsageInformation; + } + + @Override + public ObfuscationMapping getObfuscationMapping() { + return NullMapping.INSTANCE; + } + + @Override + public String toString() { + return sb.toString(); + } + + @Override + public void addSummaryError(Method method, String s) {} + + @Override + public void close() { + } + + @Override + public boolean canEmitClass(JavaTypeInstance type) { + return emitted.add(type); + } + + @Override + public int getOutputCount() { + return outputCount; + } + + @Override + public Dumper dump(JavaTypeInstance type) { + return dump(type, TypeContext.None, false); + } + + @Override + public Dumper dump(JavaTypeInstance type, boolean defines) { + return dump(type, TypeContext.None, false); + } + + @Override + public Dumper dump(JavaTypeInstance type, TypeContext context) { + return dump(type, context, false); + } + + private Dumper dump(JavaTypeInstance type, TypeContext context, boolean defines) { + doIndent(); + if (type instanceof JavaRefTypeInstance) { + int start = position; + type.dumpInto(this, typeUsageInformation, TypeContext.None); + int end = position; + Token token = new Token(start, end, sb.toString().substring(start, end)); + + if (defines) { + index.addDeclaration(token, getClassEntry(type)); + } else { + index.addReference(token, getClassEntry(type), null); + } + + return this; + } + + type.dumpInto(this, typeUsageInformation, context); + return this; + } + + @Override + public Dumper withTypeUsageInformation(TypeUsageInformation innerclassTypeUsageInformation) { + return new WithTypeUsageInformationDumper(this, innerclassTypeUsageInformation); + } + + public SourceIndex getIndex() { + index.setSource(getString()); + return index; + } + + public String getString() { + return sb.toString(); + } + + public static class WithTypeUsageInformationDumper extends DelegatingDumper { + private final TypeUsageInformation typeUsageInformation; + + WithTypeUsageInformationDumper(Dumper delegate, TypeUsageInformation typeUsageInformation) { + super(delegate); + this.typeUsageInformation = typeUsageInformation; + } + + @Override + public TypeUsageInformation getTypeUsageInformation() { + return typeUsageInformation; + } + + @Override + public Dumper dump(JavaTypeInstance javaTypeInstance) { + return dump(javaTypeInstance, TypeContext.None); + } + + @Override + public Dumper dump(JavaTypeInstance javaTypeInstance, TypeContext typeContext) { + javaTypeInstance.dumpInto(this, typeUsageInformation, typeContext); + return this; + } + + @Override + public Dumper withTypeUsageInformation(TypeUsageInformation innerclassTypeUsageInformation) { + return new WithTypeUsageInformationDumper(delegate, innerclassTypeUsageInformation); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/EntryParser.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/EntryParser.java new file mode 100644 index 0000000..2fae61a --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/EntryParser.java @@ -0,0 +1,49 @@ +package cuchaz.enigma.source.procyon; + +import com.strobel.assembler.metadata.FieldDefinition; +import com.strobel.assembler.metadata.MethodDefinition; +import com.strobel.assembler.metadata.TypeDefinition; +import com.strobel.assembler.metadata.TypeReference; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.Signature; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassDefEntry; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.FieldDefEntry; +import cuchaz.enigma.translation.representation.entry.MethodDefEntry; + +public class EntryParser { + public static FieldDefEntry parse(FieldDefinition definition) { + ClassEntry owner = parse(definition.getDeclaringType()); + TypeDescriptor descriptor = new TypeDescriptor(definition.getErasedSignature()); + Signature signature = Signature.createTypedSignature(definition.getSignature()); + AccessFlags access = new AccessFlags(definition.getModifiers()); + return new FieldDefEntry(owner, definition.getName(), descriptor, signature, access, null); + } + + public static ClassDefEntry parse(TypeDefinition def) { + String name = def.getInternalName(); + Signature signature = Signature.createSignature(def.getSignature()); + AccessFlags access = new AccessFlags(def.getModifiers()); + ClassEntry superClass = def.getBaseType() != null ? parse(def.getBaseType()) : null; + ClassEntry[] interfaces = def.getExplicitInterfaces().stream().map(EntryParser::parse).toArray(ClassEntry[]::new); + return new ClassDefEntry(name, signature, access, superClass, interfaces); + } + + public static ClassEntry parse(TypeReference typeReference) { + return new ClassEntry(typeReference.getInternalName()); + } + + public static MethodDefEntry parse(MethodDefinition definition) { + ClassEntry classEntry = parse(definition.getDeclaringType()); + MethodDescriptor descriptor = new MethodDescriptor(definition.getErasedSignature()); + Signature signature = Signature.createSignature(definition.getSignature()); + AccessFlags access = new AccessFlags(definition.getModifiers()); + return new MethodDefEntry(classEntry, definition.getName(), descriptor, signature, access, null); + } + + public static TypeDescriptor parseTypeDescriptor(TypeReference type) { + return new TypeDescriptor(type.getErasedSignature()); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/ProcyonDecompiler.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/ProcyonDecompiler.java new file mode 100644 index 0000000..3cbd680 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/ProcyonDecompiler.java @@ -0,0 +1,84 @@ +package cuchaz.enigma.source.procyon; + +import com.strobel.assembler.metadata.ITypeLoader; +import com.strobel.assembler.metadata.MetadataSystem; +import com.strobel.assembler.metadata.TypeDefinition; +import com.strobel.assembler.metadata.TypeReference; +import com.strobel.decompiler.DecompilerContext; +import com.strobel.decompiler.DecompilerSettings; +import com.strobel.decompiler.languages.java.BraceStyle; +import com.strobel.decompiler.languages.java.JavaFormattingOptions; +import com.strobel.decompiler.languages.java.ast.AstBuilder; +import com.strobel.decompiler.languages.java.ast.CompilationUnit; +import com.strobel.decompiler.languages.java.ast.InsertParenthesesVisitor; +import cuchaz.enigma.ClassProvider; +import cuchaz.enigma.source.Source; +import cuchaz.enigma.source.Decompiler; +import cuchaz.enigma.source.SourceSettings; +import cuchaz.enigma.source.procyon.transformers.*; +import cuchaz.enigma.source.procyon.typeloader.CompiledSourceTypeLoader; +import cuchaz.enigma.source.procyon.typeloader.NoRetryMetadataSystem; +import cuchaz.enigma.source.procyon.typeloader.SynchronizedTypeLoader; + +public class ProcyonDecompiler implements Decompiler { + private final SourceSettings settings; + private final DecompilerSettings decompilerSettings; + private final MetadataSystem metadataSystem; + + public ProcyonDecompiler(ClassProvider classProvider, SourceSettings settings) { + ITypeLoader typeLoader = new SynchronizedTypeLoader(new CompiledSourceTypeLoader(classProvider)); + + metadataSystem = new NoRetryMetadataSystem(typeLoader); + metadataSystem.setEagerMethodLoadingEnabled(true); + + decompilerSettings = DecompilerSettings.javaDefaults(); + decompilerSettings.setMergeVariables(getSystemPropertyAsBoolean("enigma.mergeVariables", true)); + decompilerSettings.setForceExplicitImports(getSystemPropertyAsBoolean("enigma.forceExplicitImports", true)); + decompilerSettings.setForceExplicitTypeArguments(getSystemPropertyAsBoolean("enigma.forceExplicitTypeArguments", true)); + decompilerSettings.setShowDebugLineNumbers(getSystemPropertyAsBoolean("enigma.showDebugLineNumbers", false)); + decompilerSettings.setShowSyntheticMembers(getSystemPropertyAsBoolean("enigma.showSyntheticMembers", false)); + decompilerSettings.setTypeLoader(typeLoader); + + JavaFormattingOptions formattingOptions = decompilerSettings.getJavaFormattingOptions(); + formattingOptions.ClassBraceStyle = BraceStyle.EndOfLine; + formattingOptions.InterfaceBraceStyle = BraceStyle.EndOfLine; + formattingOptions.EnumBraceStyle = BraceStyle.EndOfLine; + + this.settings = settings; + } + + @Override + public Source getSource(String className) { + TypeReference type = metadataSystem.lookupType(className); + if (type == null) { + throw new Error(String.format("Unable to find desc: %s", className)); + } + + TypeDefinition resolvedType = type.resolve(); + + DecompilerContext context = new DecompilerContext(); + context.setCurrentType(resolvedType); + context.setSettings(decompilerSettings); + + AstBuilder builder = new AstBuilder(context); + builder.addType(resolvedType); + builder.runTransformations(null); + CompilationUnit source = builder.getCompilationUnit(); + + new ObfuscatedEnumSwitchRewriterTransform(context).run(source); + new VarargsFixer(context).run(source); + new RemoveObjectCasts(context).run(source); + new Java8Generics().run(source); + new InvalidIdentifierFix().run(source); + if (settings.removeImports) DropImportAstTransform.INSTANCE.run(source); + if (settings.removeVariableFinal) DropVarModifiersAstTransform.INSTANCE.run(source); + source.acceptVisitor(new InsertParenthesesVisitor(), null); + + return new ProcyonSource(source, decompilerSettings); + } + + private static boolean getSystemPropertyAsBoolean(String property, boolean defValue) { + String value = System.getProperty(property); + return value == null ? defValue : Boolean.parseBoolean(value); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/ProcyonSource.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/ProcyonSource.java new file mode 100644 index 0000000..53c8c70 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/ProcyonSource.java @@ -0,0 +1,49 @@ +package cuchaz.enigma.source.procyon; + +import com.strobel.decompiler.DecompilerSettings; +import com.strobel.decompiler.PlainTextOutput; +import com.strobel.decompiler.languages.java.JavaOutputVisitor; +import com.strobel.decompiler.languages.java.ast.CompilationUnit; +import cuchaz.enigma.source.Source; +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.source.procyon.index.SourceIndexVisitor; +import cuchaz.enigma.source.procyon.transformers.AddJavadocsAstTransform; +import cuchaz.enigma.translation.mapping.EntryRemapper; + +import java.io.StringWriter; + +public class ProcyonSource implements Source { + private final DecompilerSettings settings; + private final CompilationUnit tree; + private String string; + + public ProcyonSource(CompilationUnit tree, DecompilerSettings settings) { + this.settings = settings; + this.tree = tree; + } + + @Override + public SourceIndex index() { + SourceIndex index = new SourceIndex(asString()); + tree.acceptVisitor(new SourceIndexVisitor(), index); + return index; + } + + @Override + public String asString() { + if (string == null) { + StringWriter writer = new StringWriter(); + tree.acceptVisitor(new JavaOutputVisitor(new PlainTextOutput(writer), settings), null); + string = writer.toString(); + } + + return string; + } + + @Override + public Source addJavadocs(EntryRemapper remapper) { + CompilationUnit remappedTree = (CompilationUnit) tree.clone(); + new AddJavadocsAstTransform(remapper).run(remappedTree); + return new ProcyonSource(remappedTree, settings); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/index/SourceIndexClassVisitor.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/index/SourceIndexClassVisitor.java new file mode 100644 index 0000000..f6eeb15 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/index/SourceIndexClassVisitor.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.source.procyon.index; + +import com.strobel.assembler.metadata.FieldDefinition; +import com.strobel.assembler.metadata.MethodDefinition; +import com.strobel.assembler.metadata.TypeDefinition; +import com.strobel.assembler.metadata.TypeReference; +import com.strobel.decompiler.languages.TextLocation; +import com.strobel.decompiler.languages.java.ast.*; +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.source.procyon.EntryParser; +import cuchaz.enigma.translation.representation.entry.*; + +public class SourceIndexClassVisitor extends SourceIndexVisitor { + private ClassDefEntry classEntry; + + public SourceIndexClassVisitor(ClassDefEntry classEntry) { + this.classEntry = classEntry; + } + + @Override + public Void visitTypeDeclaration(TypeDeclaration node, SourceIndex index) { + // is this this class, or a subtype? + TypeDefinition def = node.getUserData(Keys.TYPE_DEFINITION); + ClassDefEntry classEntry = EntryParser.parse(def); + if (!classEntry.equals(this.classEntry)) { + // it's a subtype, recurse + index.addDeclaration(TokenFactory.createToken(index, node.getNameToken()), classEntry); + return node.acceptVisitor(new SourceIndexClassVisitor(classEntry), index); + } + + return visitChildren(node, index); + } + + @Override + public Void visitSimpleType(SimpleType node, SourceIndex index) { + TypeReference ref = node.getUserData(Keys.TYPE_REFERENCE); + if (node.getIdentifierToken().getStartLocation() != TextLocation.EMPTY) { + ClassEntry classEntry = new ClassEntry(ref.getInternalName()); + index.addReference(TokenFactory.createToken(index, node.getIdentifierToken()), classEntry, this.classEntry); + } + + return visitChildren(node, index); + } + + @Override + public Void visitMethodDeclaration(MethodDeclaration node, SourceIndex index) { + MethodDefinition def = node.getUserData(Keys.METHOD_DEFINITION); + MethodDefEntry methodEntry = EntryParser.parse(def); + AstNode tokenNode = node.getNameToken(); + if (methodEntry.isConstructor() && methodEntry.getName().equals("")) { + // for static initializers, check elsewhere for the token node + tokenNode = node.getModifiers().firstOrNullObject(); + } + index.addDeclaration(TokenFactory.createToken(index, tokenNode), methodEntry); + return node.acceptVisitor(new SourceIndexMethodVisitor(methodEntry), index); + } + + @Override + public Void visitConstructorDeclaration(ConstructorDeclaration node, SourceIndex index) { + MethodDefinition def = node.getUserData(Keys.METHOD_DEFINITION); + MethodDefEntry methodEntry = EntryParser.parse(def); + index.addDeclaration(TokenFactory.createToken(index, node.getNameToken()), methodEntry); + return node.acceptVisitor(new SourceIndexMethodVisitor(methodEntry), index); + } + + @Override + public Void visitFieldDeclaration(FieldDeclaration node, SourceIndex index) { + FieldDefinition def = node.getUserData(Keys.FIELD_DEFINITION); + FieldDefEntry fieldEntry = EntryParser.parse(def); + assert (node.getVariables().size() == 1); + VariableInitializer variable = node.getVariables().firstOrNullObject(); + index.addDeclaration(TokenFactory.createToken(index, variable.getNameToken()), fieldEntry); + return visitChildren(node, index); + } + + @Override + public Void visitEnumValueDeclaration(EnumValueDeclaration node, SourceIndex index) { + // treat enum declarations as field declarations + FieldDefinition def = node.getUserData(Keys.FIELD_DEFINITION); + FieldDefEntry fieldEntry = EntryParser.parse(def); + index.addDeclaration(TokenFactory.createToken(index, node.getNameToken()), fieldEntry); + return visitChildren(node, index); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/index/SourceIndexMethodVisitor.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/index/SourceIndexMethodVisitor.java new file mode 100644 index 0000000..0e8bc51 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/index/SourceIndexMethodVisitor.java @@ -0,0 +1,218 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.source.procyon.index; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.strobel.assembler.metadata.*; +import com.strobel.decompiler.ast.Variable; +import com.strobel.decompiler.languages.TextLocation; +import com.strobel.decompiler.languages.java.ast.*; +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.source.procyon.EntryParser; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.*; + +import java.lang.Error; +import java.util.HashMap; +import java.util.Map; + +public class SourceIndexMethodVisitor extends SourceIndexVisitor { + private final MethodDefEntry methodEntry; + + private Multimap unmatchedIdentifier = HashMultimap.create(); + private Map> identifierEntryCache = new HashMap<>(); + + public SourceIndexMethodVisitor(MethodDefEntry methodEntry) { + this.methodEntry = methodEntry; + } + + @Override + public Void visitInvocationExpression(InvocationExpression node, SourceIndex index) { + MemberReference ref = node.getUserData(Keys.MEMBER_REFERENCE); + + // get the behavior entry + ClassEntry classEntry = new ClassEntry(ref.getDeclaringType().getInternalName()); + MethodEntry methodEntry = null; + if (ref instanceof MethodReference) { + methodEntry = new MethodEntry(classEntry, ref.getName(), new MethodDescriptor(ref.getErasedSignature())); + } + if (methodEntry != null) { + // get the node for the token + AstNode tokenNode = null; + if (node.getTarget() instanceof MemberReferenceExpression) { + tokenNode = ((MemberReferenceExpression) node.getTarget()).getMemberNameToken(); + } else if (node.getTarget() instanceof SuperReferenceExpression) { + tokenNode = node.getTarget(); + } else if (node.getTarget() instanceof ThisReferenceExpression) { + tokenNode = node.getTarget(); + } + if (tokenNode != null) { + index.addReference(TokenFactory.createToken(index, tokenNode), methodEntry, this.methodEntry); + } + } + + // Check for identifier + node.getArguments().stream().filter(expression -> expression instanceof IdentifierExpression) + .forEach(expression -> this.checkIdentifier((IdentifierExpression) expression, index)); + return visitChildren(node, index); + } + + @Override + public Void visitMemberReferenceExpression(MemberReferenceExpression node, SourceIndex index) { + MemberReference ref = node.getUserData(Keys.MEMBER_REFERENCE); + if (ref instanceof FieldReference) { + // make sure this is actually a field + String erasedSignature = ref.getErasedSignature(); + if (erasedSignature.indexOf('(') >= 0) { + throw new Error("Expected a field here! got " + ref); + } + + ClassEntry classEntry = new ClassEntry(ref.getDeclaringType().getInternalName()); + FieldEntry fieldEntry = new FieldEntry(classEntry, ref.getName(), new TypeDescriptor(erasedSignature)); + index.addReference(TokenFactory.createToken(index, node.getMemberNameToken()), fieldEntry, this.methodEntry); + } + + return visitChildren(node, index); + } + + @Override + public Void visitSimpleType(SimpleType node, SourceIndex index) { + TypeReference ref = node.getUserData(Keys.TYPE_REFERENCE); + if (node.getIdentifierToken().getStartLocation() != TextLocation.EMPTY) { + ClassEntry classEntry = new ClassEntry(ref.getInternalName()); + index.addReference(TokenFactory.createToken(index, node.getIdentifierToken()), classEntry, this.methodEntry); + } + + return visitChildren(node, index); + } + + @Override + public Void visitParameterDeclaration(ParameterDeclaration node, SourceIndex index) { + ParameterDefinition def = node.getUserData(Keys.PARAMETER_DEFINITION); + int parameterIndex = def.getSlot(); + + if (parameterIndex >= 0) { + MethodDefEntry ownerMethod = methodEntry; + if (def.getMethod() instanceof MethodDefinition) { + ownerMethod = EntryParser.parse((MethodDefinition) def.getMethod()); + } + + TypeDescriptor parameterType = EntryParser.parseTypeDescriptor(def.getParameterType()); + LocalVariableDefEntry localVariableEntry = new LocalVariableDefEntry(ownerMethod, parameterIndex, node.getName(), true, parameterType, null); + Identifier identifier = node.getNameToken(); + // cache the argument entry and the identifier + identifierEntryCache.put(identifier.getName(), localVariableEntry); + index.addDeclaration(TokenFactory.createToken(index, identifier), localVariableEntry); + } + + return visitChildren(node, index); + } + + @Override + public Void visitIdentifierExpression(IdentifierExpression node, SourceIndex index) { + MemberReference ref = node.getUserData(Keys.MEMBER_REFERENCE); + if (ref != null) { + ClassEntry classEntry = new ClassEntry(ref.getDeclaringType().getInternalName()); + FieldEntry fieldEntry = new FieldEntry(classEntry, ref.getName(), new TypeDescriptor(ref.getErasedSignature())); + index.addReference(TokenFactory.createToken(index, node.getIdentifierToken()), fieldEntry, this.methodEntry); + } else + this.checkIdentifier(node, index); + return visitChildren(node, index); + } + + private void checkIdentifier(IdentifierExpression node, SourceIndex index) { + if (identifierEntryCache.containsKey(node.getIdentifier())) // If it's in the argument cache, create a token! + index.addDeclaration(TokenFactory.createToken(index, node.getIdentifierToken()), identifierEntryCache.get(node.getIdentifier())); + else + unmatchedIdentifier.put(node.getIdentifier(), node.getIdentifierToken()); // Not matched actually, put it! + } + + private void addDeclarationToUnmatched(String key, SourceIndex index) { + Entry entry = identifierEntryCache.get(key); + + // This cannot happened in theory + if (entry == null) + return; + for (Identifier identifier : unmatchedIdentifier.get(key)) + index.addDeclaration(TokenFactory.createToken(index, identifier), entry); + unmatchedIdentifier.removeAll(key); + } + + @Override + public Void visitObjectCreationExpression(ObjectCreationExpression node, SourceIndex index) { + MemberReference ref = node.getUserData(Keys.MEMBER_REFERENCE); + if (ref != null && node.getType() instanceof SimpleType) { + SimpleType simpleTypeNode = (SimpleType) node.getType(); + ClassEntry classEntry = new ClassEntry(ref.getDeclaringType().getInternalName()); + MethodEntry constructorEntry = new MethodEntry(classEntry, "", new MethodDescriptor(ref.getErasedSignature())); + index.addReference(TokenFactory.createToken(index, simpleTypeNode.getIdentifierToken()), constructorEntry, this.methodEntry); + } + + return visitChildren(node, index); + } + + @Override + public Void visitVariableDeclaration(VariableDeclarationStatement node, SourceIndex index) { + AstNodeCollection variables = node.getVariables(); + + // Single assignation + if (variables.size() == 1) { + VariableInitializer initializer = variables.firstOrNullObject(); + if (initializer != null && node.getType() instanceof SimpleType) { + Identifier identifier = initializer.getNameToken(); + Variable variable = initializer.getUserData(Keys.VARIABLE); + if (variable != null) { + VariableDefinition originalVariable = variable.getOriginalVariable(); + if (originalVariable != null) { + int variableIndex = originalVariable.getSlot(); + if (variableIndex >= 0) { + MethodDefEntry ownerMethod = EntryParser.parse(originalVariable.getDeclaringMethod()); + TypeDescriptor variableType = EntryParser.parseTypeDescriptor(originalVariable.getVariableType()); + LocalVariableDefEntry localVariableEntry = new LocalVariableDefEntry(ownerMethod, variableIndex, initializer.getName(), false, variableType, null); + identifierEntryCache.put(identifier.getName(), localVariableEntry); + addDeclarationToUnmatched(identifier.getName(), index); + index.addDeclaration(TokenFactory.createToken(index, identifier), localVariableEntry); + } + } + } + } + } + return visitChildren(node, index); + } + + @Override + public Void visitMethodGroupExpression(MethodGroupExpression node, SourceIndex index) { + MemberReference ref = node.getUserData(Keys.MEMBER_REFERENCE); + + if (ref instanceof MethodReference) { + // get the behavior entry + ClassEntry classEntry = new ClassEntry(ref.getDeclaringType().getInternalName()); + MethodEntry methodEntry = new MethodEntry(classEntry, ref.getName(), new MethodDescriptor(ref.getErasedSignature())); + + // get the node for the token + AstNode methodNameToken = node.getMethodNameToken(); + AstNode targetToken = node.getTarget(); + + if (methodNameToken != null) { + index.addReference(TokenFactory.createToken(index, methodNameToken), methodEntry, this.methodEntry); + } + + if (targetToken != null && !(targetToken instanceof ThisReferenceExpression)) { + index.addReference(TokenFactory.createToken(index, targetToken), methodEntry.getParent(), this.methodEntry); + } + } + + return visitChildren(node, index); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/index/SourceIndexVisitor.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/index/SourceIndexVisitor.java new file mode 100644 index 0000000..dad505f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/index/SourceIndexVisitor.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.source.procyon.index; + +import com.strobel.assembler.metadata.TypeDefinition; +import com.strobel.decompiler.languages.java.ast.AstNode; +import com.strobel.decompiler.languages.java.ast.DepthFirstAstVisitor; +import com.strobel.decompiler.languages.java.ast.Keys; +import com.strobel.decompiler.languages.java.ast.TypeDeclaration; +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.source.procyon.EntryParser; +import cuchaz.enigma.translation.representation.entry.ClassDefEntry; + +public class SourceIndexVisitor extends DepthFirstAstVisitor { + @Override + public Void visitTypeDeclaration(TypeDeclaration node, SourceIndex index) { + TypeDefinition def = node.getUserData(Keys.TYPE_DEFINITION); + ClassDefEntry classEntry = EntryParser.parse(def); + index.addDeclaration(TokenFactory.createToken(index, node.getNameToken()), classEntry); + + return node.acceptVisitor(new SourceIndexClassVisitor(classEntry), index); + } + + @Override + protected Void visitChildren(AstNode node, SourceIndex index) { + for (final AstNode child : node.getChildren()) { + child.acceptVisitor(this, index); + } + return null; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/index/TokenFactory.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/index/TokenFactory.java new file mode 100644 index 0000000..dc36865 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/index/TokenFactory.java @@ -0,0 +1,46 @@ +package cuchaz.enigma.source.procyon.index; + +import com.strobel.decompiler.languages.Region; +import com.strobel.decompiler.languages.java.ast.AstNode; +import com.strobel.decompiler.languages.java.ast.ConstructorDeclaration; +import com.strobel.decompiler.languages.java.ast.Identifier; +import com.strobel.decompiler.languages.java.ast.TypeDeclaration; +import cuchaz.enigma.source.Token; +import cuchaz.enigma.source.SourceIndex; + +import java.util.regex.Pattern; + +public class TokenFactory { + private static final Pattern ANONYMOUS_INNER = Pattern.compile("\\$\\d+$"); + + public static Token createToken(SourceIndex index, AstNode node) { + String name = node instanceof Identifier ? ((Identifier) node).getName() : ""; + Region region = node.getRegion(); + + if (region.getBeginLine() == 0) { + System.err.println("Got bad region from Procyon for node " + node); + return null; + } + + int start = index.getPosition(region.getBeginLine(), region.getBeginColumn()); + int end = index.getPosition(region.getEndLine(), region.getEndColumn()); + String text = index.getSource().substring(start, end); + Token token = new Token(start, end, text); + + boolean isAnonymousInner = + node instanceof Identifier && + name.indexOf('$') >= 0 && node.getParent() instanceof ConstructorDeclaration && + name.lastIndexOf('$') >= 0 && + !ANONYMOUS_INNER.matcher(name).matches(); + + if (isAnonymousInner) { + TypeDeclaration type = node.getParent().getParent() instanceof TypeDeclaration ? (TypeDeclaration) node.getParent().getParent() : null; + if (type != null) { + name = type.getName(); + token.end = token.start + name.length(); + } + } + + return token; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/AddJavadocsAstTransform.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/AddJavadocsAstTransform.java new file mode 100644 index 0000000..70fc8c6 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/AddJavadocsAstTransform.java @@ -0,0 +1,134 @@ +package cuchaz.enigma.source.procyon.transformers; + +import com.google.common.base.Function; +import com.google.common.base.Strings; +import com.strobel.assembler.metadata.ParameterDefinition; +import com.strobel.decompiler.languages.java.ast.*; +import com.strobel.decompiler.languages.java.ast.transforms.IAstTransform; +import cuchaz.enigma.source.procyon.EntryParser; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; +import cuchaz.enigma.translation.representation.entry.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +public final class AddJavadocsAstTransform implements IAstTransform { + + private final EntryRemapper remapper; + + public AddJavadocsAstTransform(EntryRemapper remapper) { + this.remapper = remapper; + } + + @Override + public void run(AstNode compilationUnit) { + compilationUnit.acceptVisitor(new Visitor(remapper), null); + } + + static class Visitor extends DepthFirstAstVisitor { + + private final EntryRemapper remapper; + + Visitor(EntryRemapper remapper) { + this.remapper = remapper; + } + + private void addDoc(T node, Function> retriever) { + final Comment[] comments = getComments(node, retriever); + if (comments != null) { + node.insertChildrenBefore(node.getFirstChild(), Roles.COMMENT, comments); + } + } + + private Comment[] getComments(T node, Function> retriever) { + final EntryMapping mapping = remapper.getDeobfMapping(retriever.apply(node)); + final String docs = mapping == null ? null : Strings.emptyToNull(mapping.getJavadoc()); + return docs == null ? null : Stream.of(docs.split("\\R")).map(st -> new Comment(st, + CommentType.Documentation)).toArray(Comment[]::new); + } + + private Comment[] getParameterComments(ParameterDeclaration node, Function> retriever) { + final EntryMapping mapping = remapper.getDeobfMapping(retriever.apply(node)); + final Comment[] ret = getComments(node, retriever); + if (ret != null) { + final String paramPrefix = "@param " + mapping.getTargetName() + " "; + final String indent = Strings.repeat(" ", paramPrefix.length()); + ret[0].setContent(paramPrefix + ret[0].getContent()); + for (int i = 1; i < ret.length; i++) { + ret[i].setContent(indent + ret[i].getContent()); + } + } + return ret; + } + + private void visitMethod(AstNode node) { + final MethodDefEntry methodDefEntry = EntryParser.parse(node.getUserData(Keys.METHOD_DEFINITION)); + final Comment[] baseComments = getComments(node, $ -> methodDefEntry); + List comments = new ArrayList<>(); + if (baseComments != null) + Collections.addAll(comments, baseComments); + + for (ParameterDeclaration dec : node.getChildrenByRole(Roles.PARAMETER)) { + ParameterDefinition def = dec.getUserData(Keys.PARAMETER_DEFINITION); + final Comment[] paramComments = getParameterComments(dec, $ -> new LocalVariableDefEntry(methodDefEntry, def.getSlot(), def.getName(), + true, + EntryParser.parseTypeDescriptor(def.getParameterType()), null)); + if (paramComments != null) + Collections.addAll(comments, paramComments); + } + + if (!comments.isEmpty()) { + if (remapper.getObfResolver().resolveEntry(methodDefEntry, ResolutionStrategy.RESOLVE_ROOT).stream().noneMatch(e -> Objects.equals(e, methodDefEntry))) { + comments.add(0, new Comment("{@inheritDoc}", CommentType.Documentation)); + } + final AstNode oldFirst = node.getFirstChild(); + for (Comment comment : comments) { + node.insertChildBefore(oldFirst, comment, Roles.COMMENT); + } + } + } + + @Override + protected Void visitChildren(AstNode node, Void data) { + for (final AstNode child : node.getChildren()) { + child.acceptVisitor(this, data); + } + return null; + } + + @Override + public Void visitMethodDeclaration(MethodDeclaration node, Void data) { + visitMethod(node); + return super.visitMethodDeclaration(node, data); + } + + @Override + public Void visitConstructorDeclaration(ConstructorDeclaration node, Void data) { + visitMethod(node); + return super.visitConstructorDeclaration(node, data); + } + + @Override + public Void visitFieldDeclaration(FieldDeclaration node, Void data) { + addDoc(node, dec -> EntryParser.parse(dec.getUserData(Keys.FIELD_DEFINITION))); + return super.visitFieldDeclaration(node, data); + } + + @Override + public Void visitTypeDeclaration(TypeDeclaration node, Void data) { + addDoc(node, dec -> EntryParser.parse(dec.getUserData(Keys.TYPE_DEFINITION))); + return super.visitTypeDeclaration(node, data); + } + + @Override + public Void visitEnumValueDeclaration(EnumValueDeclaration node, Void data) { + addDoc(node, dec -> EntryParser.parse(dec.getUserData(Keys.FIELD_DEFINITION))); + return super.visitEnumValueDeclaration(node, data); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/DropImportAstTransform.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/DropImportAstTransform.java new file mode 100644 index 0000000..39e599d --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/DropImportAstTransform.java @@ -0,0 +1,33 @@ +package cuchaz.enigma.source.procyon.transformers; + +import com.strobel.decompiler.languages.java.ast.AstNode; +import com.strobel.decompiler.languages.java.ast.DepthFirstAstVisitor; +import com.strobel.decompiler.languages.java.ast.ImportDeclaration; +import com.strobel.decompiler.languages.java.ast.PackageDeclaration; +import com.strobel.decompiler.languages.java.ast.transforms.IAstTransform; + +public final class DropImportAstTransform implements IAstTransform { + public static final DropImportAstTransform INSTANCE = new DropImportAstTransform(); + + private DropImportAstTransform() { + } + + @Override + public void run(AstNode compilationUnit) { + compilationUnit.acceptVisitor(new Visitor(), null); + } + + static class Visitor extends DepthFirstAstVisitor { + @Override + public Void visitPackageDeclaration(PackageDeclaration node, Void data) { + node.remove(); + return null; + } + + @Override + public Void visitImportDeclaration(ImportDeclaration node, Void data) { + node.remove(); + return null; + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/DropVarModifiersAstTransform.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/DropVarModifiersAstTransform.java new file mode 100644 index 0000000..b8c087b --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/DropVarModifiersAstTransform.java @@ -0,0 +1,37 @@ +package cuchaz.enigma.source.procyon.transformers; + +import com.strobel.decompiler.languages.java.ast.*; +import com.strobel.decompiler.languages.java.ast.transforms.IAstTransform; + +import javax.lang.model.element.Modifier; + +public final class DropVarModifiersAstTransform implements IAstTransform { + public static final DropVarModifiersAstTransform INSTANCE = new DropVarModifiersAstTransform(); + + private DropVarModifiersAstTransform() { + } + + @Override + public void run(AstNode compilationUnit) { + compilationUnit.acceptVisitor(new Visitor(), null); + } + + static class Visitor extends DepthFirstAstVisitor { + @Override + public Void visitParameterDeclaration(ParameterDeclaration node, Void data) { + for (JavaModifierToken modifierToken : node.getChildrenByRole(EntityDeclaration.MODIFIER_ROLE)) { + if (modifierToken.getModifier() == Modifier.FINAL) { + modifierToken.remove(); + } + } + + return null; + } + + @Override + public Void visitVariableDeclaration(VariableDeclarationStatement node, Void data) { + node.removeModifier(Modifier.FINAL); + return null; + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/InvalidIdentifierFix.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/InvalidIdentifierFix.java new file mode 100644 index 0000000..34d95fa --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/InvalidIdentifierFix.java @@ -0,0 +1,29 @@ +package cuchaz.enigma.source.procyon.transformers; + +import com.strobel.decompiler.languages.java.ast.AstNode; +import com.strobel.decompiler.languages.java.ast.DepthFirstAstVisitor; +import com.strobel.decompiler.languages.java.ast.Identifier; +import com.strobel.decompiler.languages.java.ast.transforms.IAstTransform; + +/** + * Created by Thiakil on 13/07/2018. + */ +public class InvalidIdentifierFix implements IAstTransform { + @Override + public void run(AstNode compilationUnit) { + compilationUnit.acceptVisitor(new Visitor(), null); + } + + class Visitor extends DepthFirstAstVisitor{ + @Override + public Void visitIdentifier(Identifier node, Void data) { + super.visitIdentifier(node, data); + if (node.getName().equals("do") || node.getName().equals("if")){ + Identifier newIdentifier = Identifier.create(node.getName() + "_", node.getStartLocation()); + newIdentifier.copyUserDataFrom(node); + node.replaceWith(newIdentifier); + } + return null; + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/Java8Generics.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/Java8Generics.java new file mode 100644 index 0000000..8accfc7 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/Java8Generics.java @@ -0,0 +1,107 @@ +package cuchaz.enigma.source.procyon.transformers; + +import com.strobel.assembler.metadata.BuiltinTypes; +import com.strobel.assembler.metadata.CommonTypeReferences; +import com.strobel.assembler.metadata.Flags; +import com.strobel.assembler.metadata.IGenericInstance; +import com.strobel.assembler.metadata.IMemberDefinition; +import com.strobel.assembler.metadata.JvmType; +import com.strobel.assembler.metadata.MemberReference; +import com.strobel.assembler.metadata.MethodDefinition; +import com.strobel.assembler.metadata.TypeDefinition; +import com.strobel.assembler.metadata.TypeReference; +import com.strobel.decompiler.languages.java.ast.ArrayCreationExpression; +import com.strobel.decompiler.languages.java.ast.AstNode; +import com.strobel.decompiler.languages.java.ast.AstNodeCollection; +import com.strobel.decompiler.languages.java.ast.AstType; +import com.strobel.decompiler.languages.java.ast.CastExpression; +import com.strobel.decompiler.languages.java.ast.ComposedType; +import com.strobel.decompiler.languages.java.ast.DepthFirstAstVisitor; +import com.strobel.decompiler.languages.java.ast.Expression; +import com.strobel.decompiler.languages.java.ast.Identifier; +import com.strobel.decompiler.languages.java.ast.InvocationExpression; +import com.strobel.decompiler.languages.java.ast.Keys; +import com.strobel.decompiler.languages.java.ast.MemberReferenceExpression; +import com.strobel.decompiler.languages.java.ast.ObjectCreationExpression; +import com.strobel.decompiler.languages.java.ast.Roles; +import com.strobel.decompiler.languages.java.ast.SimpleType; +import com.strobel.decompiler.languages.java.ast.WildcardType; +import com.strobel.decompiler.languages.java.ast.transforms.IAstTransform; + +/** + * Created by Thiakil on 12/07/2018. + */ +public class Java8Generics implements IAstTransform { + + @Override + public void run(AstNode compilationUnit) { + compilationUnit.acceptVisitor(new Visitor(), null); + } + + static class Visitor extends DepthFirstAstVisitor{ + + @Override + public Void visitInvocationExpression(InvocationExpression node, Void data) { + super.visitInvocationExpression(node, data); + if (node.getTarget() instanceof MemberReferenceExpression){ + MemberReferenceExpression referenceExpression = (MemberReferenceExpression) node.getTarget(); + if (referenceExpression.getTypeArguments().stream().map(t->{ + TypeReference tr = t.toTypeReference(); + if (tr.getDeclaringType() != null){//ensure that inner types are resolved so we can get the TypeDefinition below + TypeReference resolved = tr.resolve(); + if (resolved != null) + return resolved; + } + return tr; + }).anyMatch(t -> t.isWildcardType() || (t instanceof TypeDefinition && ((TypeDefinition) t).isAnonymous()))) { + //these are invalid for invocations, let the compiler work it out + referenceExpression.getTypeArguments().clear(); + } else if (referenceExpression.getTypeArguments().stream().allMatch(t->t.toTypeReference().equals(CommonTypeReferences.Object))){ + //all are , thereby redundant and/or bad + referenceExpression.getTypeArguments().clear(); + } + } + return null; + } + + @Override + public Void visitObjectCreationExpression(ObjectCreationExpression node, Void data) { + super.visitObjectCreationExpression(node, data); + AstType type = node.getType(); + if (type instanceof SimpleType && !((SimpleType) type).getTypeArguments().isEmpty()){ + SimpleType simpleType = (SimpleType) type; + AstNodeCollection typeArguments = simpleType.getTypeArguments(); + if (typeArguments.size() == 1 && typeArguments.firstOrNullObject().toTypeReference().equals(CommonTypeReferences.Object)){ + //all are , thereby redundant and/or bad + typeArguments.firstOrNullObject().getChildByRole(Roles.IDENTIFIER).replaceWith(Identifier.create("")); + } + } + return null; + } + + @Override + public Void visitCastExpression(CastExpression node, Void data) { + boolean doReplace = false; + TypeReference typeReference = node.getType().toTypeReference(); + if (typeReference.isArray() && typeReference.getElementType().isGenericType()){ + doReplace = true; + } else if (typeReference.isGenericType()) { + Expression target = node.getExpression(); + if (typeReference instanceof IGenericInstance && ((IGenericInstance)typeReference).getTypeArguments().stream().anyMatch(t->t.isWildcardType())){ + doReplace = true; + } else if (target instanceof InvocationExpression) { + InvocationExpression invocationExpression = (InvocationExpression)target; + if (invocationExpression.getTarget() instanceof MemberReferenceExpression && !((MemberReferenceExpression) invocationExpression.getTarget()).getTypeArguments().isEmpty()) { + ((MemberReferenceExpression) invocationExpression.getTarget()).getTypeArguments().clear(); + doReplace = true; + } + } + } + super.visitCastExpression(node, data); + if (doReplace){ + node.replaceWith(node.getExpression()); + } + return null; + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/ObfuscatedEnumSwitchRewriterTransform.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/ObfuscatedEnumSwitchRewriterTransform.java new file mode 100644 index 0000000..32bb72f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/ObfuscatedEnumSwitchRewriterTransform.java @@ -0,0 +1,414 @@ +/* + * Originally: + * EnumSwitchRewriterTransform.java + * + * Copyright (c) 2013 Mike Strobel + * + * This source code is based on Mono.Cecil from Jb Evain, Copyright (c) Jb Evain; + * and ILSpy/ICSharpCode from SharpDevelop, Copyright (c) AlphaSierraPapa. + * + * This source code is subject to terms and conditions of the Apache License, Version 2.0. + * A copy of the license can be found in the License.html file at the root of this distribution. + * By using this source code in any fashion, you are agreeing to be bound by the terms of the + * Apache License, Version 2.0. + * + * You must not remove this notice, or any other, from this software. + */ + +package cuchaz.enigma.source.procyon.transformers; + +import com.strobel.assembler.metadata.BuiltinTypes; +import com.strobel.assembler.metadata.FieldDefinition; +import com.strobel.assembler.metadata.MethodDefinition; +import com.strobel.assembler.metadata.TypeDefinition; +import com.strobel.assembler.metadata.TypeReference; +import com.strobel.core.SafeCloseable; +import com.strobel.core.VerifyArgument; +import com.strobel.decompiler.DecompilerContext; +import com.strobel.decompiler.languages.java.ast.AssignmentExpression; +import com.strobel.decompiler.languages.java.ast.AstBuilder; +import com.strobel.decompiler.languages.java.ast.AstNode; +import com.strobel.decompiler.languages.java.ast.CaseLabel; +import com.strobel.decompiler.languages.java.ast.ContextTrackingVisitor; +import com.strobel.decompiler.languages.java.ast.Expression; +import com.strobel.decompiler.languages.java.ast.IdentifierExpression; +import com.strobel.decompiler.languages.java.ast.IndexerExpression; +import com.strobel.decompiler.languages.java.ast.InvocationExpression; +import com.strobel.decompiler.languages.java.ast.Keys; +import com.strobel.decompiler.languages.java.ast.MemberReferenceExpression; +import com.strobel.decompiler.languages.java.ast.PrimitiveExpression; +import com.strobel.decompiler.languages.java.ast.SwitchSection; +import com.strobel.decompiler.languages.java.ast.SwitchStatement; +import com.strobel.decompiler.languages.java.ast.TypeDeclaration; +import com.strobel.decompiler.languages.java.ast.TypeReferenceExpression; +import com.strobel.decompiler.languages.java.ast.transforms.IAstTransform; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Copy of {@link com.strobel.decompiler.languages.java.ast.transforms.EnumSwitchRewriterTransform} modified to: + * - Not rely on a field containing "$SwitchMap$" (Proguard strips it) + * - Ignore classes *with* SwitchMap$ names (so the original can handle it) + * - Ignores inner synthetics that are not package private + */ +@SuppressWarnings("Duplicates") +public class ObfuscatedEnumSwitchRewriterTransform implements IAstTransform { + private final DecompilerContext _context; + + public ObfuscatedEnumSwitchRewriterTransform(final DecompilerContext context) { + _context = VerifyArgument.notNull(context, "context"); + } + + @Override + public void run(final AstNode compilationUnit) { + compilationUnit.acceptVisitor(new Visitor(_context), null); + } + + private final static class Visitor extends ContextTrackingVisitor { + private final static class SwitchMapInfo { + final String enclosingType; + final Map> switches = new LinkedHashMap<>(); + final Map> mappings = new LinkedHashMap<>(); + + TypeDeclaration enclosingTypeDeclaration; + + SwitchMapInfo(final String enclosingType) { + this.enclosingType = enclosingType; + } + } + + private final Map _switchMaps = new LinkedHashMap<>(); + private boolean _isSwitchMapWrapper; + + protected Visitor(final DecompilerContext context) { + super(context); + } + + @Override + public Void visitTypeDeclaration(final TypeDeclaration typeDeclaration, final Void p) { + final boolean oldIsSwitchMapWrapper = _isSwitchMapWrapper; + final TypeDefinition typeDefinition = typeDeclaration.getUserData(Keys.TYPE_DEFINITION); + final boolean isSwitchMapWrapper = isSwitchMapWrapper(typeDefinition); + + if (isSwitchMapWrapper) { + final String internalName = typeDefinition.getInternalName(); + + SwitchMapInfo info = _switchMaps.get(internalName); + + if (info == null) { + _switchMaps.put(internalName, info = new SwitchMapInfo(internalName)); + } + + info.enclosingTypeDeclaration = typeDeclaration; + } + + _isSwitchMapWrapper = isSwitchMapWrapper; + + try { + super.visitTypeDeclaration(typeDeclaration, p); + } + finally { + _isSwitchMapWrapper = oldIsSwitchMapWrapper; + } + + rewrite(); + + return null; + } + + @Override + public Void visitSwitchStatement(final SwitchStatement node, final Void data) { + final Expression test = node.getExpression(); + + if (test instanceof IndexerExpression) { + final IndexerExpression indexer = (IndexerExpression) test; + final Expression array = indexer.getTarget(); + final Expression argument = indexer.getArgument(); + + if (!(array instanceof MemberReferenceExpression)) { + return super.visitSwitchStatement(node, data); + } + + final MemberReferenceExpression arrayAccess = (MemberReferenceExpression) array; + final Expression arrayOwner = arrayAccess.getTarget(); + final String mapName = arrayAccess.getMemberName(); + + if (mapName == null || mapName.startsWith("$SwitchMap$") || !(arrayOwner instanceof TypeReferenceExpression)) { + return super.visitSwitchStatement(node, data); + } + + final TypeReferenceExpression enclosingTypeExpression = (TypeReferenceExpression) arrayOwner; + final TypeReference enclosingType = enclosingTypeExpression.getType().getUserData(Keys.TYPE_REFERENCE); + + if (!isSwitchMapWrapper(enclosingType) || !(argument instanceof InvocationExpression)) { + return super.visitSwitchStatement(node, data); + } + + final InvocationExpression invocation = (InvocationExpression) argument; + final Expression invocationTarget = invocation.getTarget(); + + if (!(invocationTarget instanceof MemberReferenceExpression)) { + return super.visitSwitchStatement(node, data); + } + + final MemberReferenceExpression memberReference = (MemberReferenceExpression) invocationTarget; + + if (!"ordinal".equals(memberReference.getMemberName())) { + return super.visitSwitchStatement(node, data); + } + + final String enclosingTypeName = enclosingType.getInternalName(); + + SwitchMapInfo info = _switchMaps.get(enclosingTypeName); + + if (info == null) { + _switchMaps.put(enclosingTypeName, info = new SwitchMapInfo(enclosingTypeName)); + + final TypeDefinition resolvedType = enclosingType.resolve(); + + if (resolvedType != null) { + AstBuilder astBuilder = context.getUserData(Keys.AST_BUILDER); + + if (astBuilder == null) { + astBuilder = new AstBuilder(context); + } + + try (final SafeCloseable importSuppression = astBuilder.suppressImports()) { + final TypeDeclaration declaration = astBuilder.createType(resolvedType); + + declaration.acceptVisitor(this, data); + } + } + } + + List switches = info.switches.get(mapName); + + if (switches == null) { + info.switches.put(mapName, switches = new ArrayList<>()); + } + + switches.add(node); + } + + return super.visitSwitchStatement(node, data); + } + + @Override + public Void visitAssignmentExpression(final AssignmentExpression node, final Void data) { + final TypeDefinition currentType = context.getCurrentType(); + final MethodDefinition currentMethod = context.getCurrentMethod(); + + if (_isSwitchMapWrapper && + currentType != null && + currentMethod != null && + currentMethod.isTypeInitializer()) { + + final Expression left = node.getLeft(); + final Expression right = node.getRight(); + + if (left instanceof IndexerExpression && + right instanceof PrimitiveExpression) { + + String mapName = null; + + final Expression array = ((IndexerExpression) left).getTarget(); + final Expression argument = ((IndexerExpression) left).getArgument(); + + if (array instanceof MemberReferenceExpression) { + mapName = ((MemberReferenceExpression) array).getMemberName(); + } + else if (array instanceof IdentifierExpression) { + mapName = ((IdentifierExpression) array).getIdentifier(); + } + + if (mapName == null || mapName.startsWith("$SwitchMap$")) { + return super.visitAssignmentExpression(node, data); + } + + if (!(argument instanceof InvocationExpression)) { + return super.visitAssignmentExpression(node, data); + } + + final InvocationExpression invocation = (InvocationExpression) argument; + final Expression invocationTarget = invocation.getTarget(); + + if (!(invocationTarget instanceof MemberReferenceExpression)) { + return super.visitAssignmentExpression(node, data); + } + + final MemberReferenceExpression memberReference = (MemberReferenceExpression) invocationTarget; + final Expression memberTarget = memberReference.getTarget(); + + if (!(memberTarget instanceof MemberReferenceExpression) || !"ordinal".equals(memberReference.getMemberName())) { + return super.visitAssignmentExpression(node, data); + } + + final MemberReferenceExpression outerMemberReference = (MemberReferenceExpression) memberTarget; + final Expression outerMemberTarget = outerMemberReference.getTarget(); + + if (!(outerMemberTarget instanceof TypeReferenceExpression)) { + return super.visitAssignmentExpression(node, data); + } + + final String enclosingType = currentType.getInternalName(); + + SwitchMapInfo info = _switchMaps.get(enclosingType); + + if (info == null) { + _switchMaps.put(enclosingType, info = new SwitchMapInfo(enclosingType)); + + AstBuilder astBuilder = context.getUserData(Keys.AST_BUILDER); + + if (astBuilder == null) { + astBuilder = new AstBuilder(context); + } + + info.enclosingTypeDeclaration = astBuilder.createType(currentType); + } + + final PrimitiveExpression value = (PrimitiveExpression) right; + + assert value.getValue() instanceof Integer; + + Map mapping = info.mappings.get(mapName); + + if (mapping == null) { + info.mappings.put(mapName, mapping = new LinkedHashMap<>()); + } + + final IdentifierExpression enumValue = new IdentifierExpression( Expression.MYSTERY_OFFSET, outerMemberReference.getMemberName()); + + enumValue.putUserData(Keys.MEMBER_REFERENCE, outerMemberReference.getUserData(Keys.MEMBER_REFERENCE)); + + mapping.put(((Number) value.getValue()).intValue(), enumValue); + } + } + + return super.visitAssignmentExpression(node, data); + } + + private void rewrite() { + if (_switchMaps.isEmpty()) { + return; + } + + for (final SwitchMapInfo info : _switchMaps.values()) { + rewrite(info); + } + + // + // Remove switch map type wrappers that are no longer referenced. + // + + outer: + for (final SwitchMapInfo info : _switchMaps.values()) { + for (final String mapName : info.switches.keySet()) { + final List switches = info.switches.get(mapName); + + if (switches != null && !switches.isEmpty()) { + continue outer; + } + } + + final TypeDeclaration enclosingTypeDeclaration = info.enclosingTypeDeclaration; + + if (enclosingTypeDeclaration != null) { + enclosingTypeDeclaration.remove(); + } + } + } + + private void rewrite(final SwitchMapInfo info) { + if (info.switches.isEmpty()) { + return; + } + + for (final String mapName : info.switches.keySet()) { + final List switches = info.switches.get(mapName); + final Map mappings = info.mappings.get(mapName); + + if (switches != null && mappings != null) { + for (int i = 0; i < switches.size(); i++) { + if (rewriteSwitch(switches.get(i), mappings)) { + switches.remove(i--); + } + } + } + } + } + + private boolean rewriteSwitch(final SwitchStatement s, final Map mappings) { + final Map replacements = new IdentityHashMap<>(); + + for (final SwitchSection section : s.getSwitchSections()) { + for (final CaseLabel caseLabel : section.getCaseLabels()) { + final Expression expression = caseLabel.getExpression(); + + if (expression.isNull()) { + continue; + } + + if (expression instanceof PrimitiveExpression) { + final Object value = ((PrimitiveExpression) expression).getValue(); + + if (value instanceof Integer) { + final Expression replacement = mappings.get(value); + + if (replacement != null) { + replacements.put(expression, replacement); + continue; + } + } + } + + // + // If we can't rewrite all cases, we abort. + // + + return false; + } + } + + final IndexerExpression indexer = (IndexerExpression) s.getExpression(); + final InvocationExpression argument = (InvocationExpression) indexer.getArgument(); + final MemberReferenceExpression memberReference = (MemberReferenceExpression) argument.getTarget(); + final Expression newTest = memberReference.getTarget(); + + newTest.remove(); + indexer.replaceWith(newTest); + + for (final Map.Entry entry : replacements.entrySet()) { + entry.getKey().replaceWith(entry.getValue().clone()); + } + + return true; + } + + private static boolean isSwitchMapWrapper(final TypeReference type) { + if (type == null) { + return false; + } + + final TypeDefinition definition = type instanceof TypeDefinition ? (TypeDefinition) type + : type.resolve(); + + if (definition == null || !definition.isSynthetic() || !definition.isInnerClass() || !definition.isPackagePrivate()) { + return false; + } + + for (final FieldDefinition field : definition.getDeclaredFields()) { + if (!field.getName().startsWith("$SwitchMap$") && + BuiltinTypes.Integer.makeArrayType().equals(field.getFieldType())) { + + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/RemoveObjectCasts.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/RemoveObjectCasts.java new file mode 100644 index 0000000..cf0376f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/RemoveObjectCasts.java @@ -0,0 +1,39 @@ +package cuchaz.enigma.source.procyon.transformers; + +import com.strobel.assembler.metadata.BuiltinTypes; +import com.strobel.decompiler.DecompilerContext; +import com.strobel.decompiler.languages.java.ast.AstNode; +import com.strobel.decompiler.languages.java.ast.CastExpression; +import com.strobel.decompiler.languages.java.ast.ContextTrackingVisitor; +import com.strobel.decompiler.languages.java.ast.transforms.IAstTransform; + +/** + * Created by Thiakil on 11/07/2018. + */ +public class RemoveObjectCasts implements IAstTransform { + private final DecompilerContext _context; + + public RemoveObjectCasts(DecompilerContext context) { + _context = context; + } + + @Override + public void run(AstNode compilationUnit) { + compilationUnit.acceptVisitor(new Visitor(_context), null); + } + + private final static class Visitor extends ContextTrackingVisitor{ + + protected Visitor(DecompilerContext context) { + super(context); + } + + @Override + public Void visitCastExpression(CastExpression node, Void data) { + if (node.getType().toTypeReference().equals(BuiltinTypes.Object)){ + node.replaceWith(node.getExpression()); + } + return super.visitCastExpression(node, data); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/VarargsFixer.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/VarargsFixer.java new file mode 100644 index 0000000..d3ddaab --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/transformers/VarargsFixer.java @@ -0,0 +1,197 @@ +package cuchaz.enigma.source.procyon.transformers; + +import com.strobel.assembler.metadata.MemberReference; +import com.strobel.assembler.metadata.MetadataFilters; +import com.strobel.assembler.metadata.MetadataHelper; +import com.strobel.assembler.metadata.MethodBinder; +import com.strobel.assembler.metadata.MethodDefinition; +import com.strobel.assembler.metadata.MethodReference; +import com.strobel.assembler.metadata.TypeReference; +import com.strobel.core.StringUtilities; +import com.strobel.core.VerifyArgument; +import com.strobel.decompiler.DecompilerContext; +import com.strobel.decompiler.languages.java.ast.ArrayCreationExpression; +import com.strobel.decompiler.languages.java.ast.ArrayInitializerExpression; +import com.strobel.decompiler.languages.java.ast.AstNode; +import com.strobel.decompiler.languages.java.ast.AstNodeCollection; +import com.strobel.decompiler.languages.java.ast.CastExpression; +import com.strobel.decompiler.languages.java.ast.ContextTrackingVisitor; +import com.strobel.decompiler.languages.java.ast.DepthFirstAstVisitor; +import com.strobel.decompiler.languages.java.ast.Expression; +import com.strobel.decompiler.languages.java.ast.InvocationExpression; +import com.strobel.decompiler.languages.java.ast.JavaResolver; +import com.strobel.decompiler.languages.java.ast.Keys; +import com.strobel.decompiler.languages.java.ast.MemberReferenceExpression; +import com.strobel.decompiler.languages.java.ast.ObjectCreationExpression; +import com.strobel.decompiler.languages.java.ast.transforms.IAstTransform; +import com.strobel.decompiler.semantics.ResolveResult; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Thiakil on 12/07/2018. + */ +public class VarargsFixer implements IAstTransform { + private final DecompilerContext _context; + + public VarargsFixer(final DecompilerContext context) { + _context = VerifyArgument.notNull(context, "context"); + } + + @Override + public void run(AstNode compilationUnit) { + compilationUnit.acceptVisitor(new Visitor(_context), null); + } + + class Visitor extends ContextTrackingVisitor { + private final JavaResolver _resolver; + protected Visitor(DecompilerContext context) { + super(context); + _resolver = new JavaResolver(context); + } + + //remove `new Object[0]` on varagrs as the normal tranformer doesnt do them + @Override + public Void visitInvocationExpression(InvocationExpression node, Void data) { + super.visitInvocationExpression(node, data); + MemberReference definition = node.getUserData(Keys.MEMBER_REFERENCE); + if (definition instanceof MethodDefinition && ((MethodDefinition) definition).isVarArgs()){ + AstNodeCollection arguments = node.getArguments(); + Expression lastParam = arguments.lastOrNullObject(); + if (!lastParam.isNull() && lastParam instanceof ArrayCreationExpression){ + ArrayCreationExpression varargArray = (ArrayCreationExpression)lastParam; + if (varargArray.getInitializer().isNull() || varargArray.getInitializer().getElements().isEmpty()){ + lastParam.remove(); + } else { + for (Expression e : varargArray.getInitializer().getElements()){ + arguments.insertBefore(varargArray, e.clone()); + } + varargArray.remove(); + } + } + } + return null; + } + + //applies the vararg transform to object creation + @Override + public Void visitObjectCreationExpression(ObjectCreationExpression node, Void data) { + super.visitObjectCreationExpression(node, data); + final AstNodeCollection arguments = node.getArguments(); + final Expression lastArgument = arguments.lastOrNullObject(); + + Expression arrayArg = lastArgument; + + if (arrayArg instanceof CastExpression) + arrayArg = ((CastExpression) arrayArg).getExpression(); + + if (arrayArg == null || + arrayArg.isNull() || + !(arrayArg instanceof ArrayCreationExpression && + node.getTarget() instanceof MemberReferenceExpression)) { + + return null; + } + + final ArrayCreationExpression newArray = (ArrayCreationExpression) arrayArg; + final MemberReferenceExpression target = (MemberReferenceExpression) node.getTarget(); + + if (!newArray.getAdditionalArraySpecifiers().hasSingleElement()) { + return null; + } + + final MethodReference method = (MethodReference) node.getUserData(Keys.MEMBER_REFERENCE); + + if (method == null) { + return null; + } + + final MethodDefinition resolved = method.resolve(); + + if (resolved == null || !resolved.isVarArgs()) { + return null; + } + + final List candidates; + final Expression invocationTarget = target.getTarget(); + + if (invocationTarget == null || invocationTarget.isNull()) { + candidates = MetadataHelper.findMethods( + context.getCurrentType(), + MetadataFilters.matchName(resolved.getName()) + ); + } + else { + final ResolveResult targetResult = _resolver.apply(invocationTarget); + + if (targetResult == null || targetResult.getType() == null) { + return null; + } + + candidates = MetadataHelper.findMethods( + targetResult.getType(), + MetadataFilters.matchName(resolved.getName()) + ); + } + + final List argTypes = new ArrayList<>(); + + for (final Expression argument : arguments) { + final ResolveResult argResult = _resolver.apply(argument); + + if (argResult == null || argResult.getType() == null) { + return null; + } + + argTypes.add(argResult.getType()); + } + + final MethodBinder.BindResult c1 = MethodBinder.selectMethod(candidates, argTypes); + + if (c1.isFailure() || c1.isAmbiguous()) { + return null; + } + + argTypes.remove(argTypes.size() - 1); + + final ArrayInitializerExpression initializer = newArray.getInitializer(); + final boolean hasElements = !initializer.isNull() && !initializer.getElements().isEmpty(); + + if (hasElements) { + for (final Expression argument : initializer.getElements()) { + final ResolveResult argResult = _resolver.apply(argument); + + if (argResult == null || argResult.getType() == null) { + return null; + } + + argTypes.add(argResult.getType()); + } + } + + final MethodBinder.BindResult c2 = MethodBinder.selectMethod(candidates, argTypes); + + if (c2.isFailure() || + c2.isAmbiguous() || + !StringUtilities.equals(c2.getMethod().getErasedSignature(), c1.getMethod().getErasedSignature())) { + + return null; + } + + lastArgument.remove(); + + if (!hasElements) { + lastArgument.remove(); + return null; + } + + for (final Expression newArg : initializer.getElements()) { + newArg.remove(); + arguments.add(newArg); + } + + return null; + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingClasspathTypeLoader.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingClasspathTypeLoader.java new file mode 100644 index 0000000..e702956 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingClasspathTypeLoader.java @@ -0,0 +1,33 @@ +package cuchaz.enigma.source.procyon.typeloader; + +import com.strobel.assembler.metadata.Buffer; +import com.strobel.assembler.metadata.ClasspathTypeLoader; +import com.strobel.assembler.metadata.ITypeLoader; + +/** + * Caching version of {@link ClasspathTypeLoader} + */ +public class CachingClasspathTypeLoader extends CachingTypeLoader { + private static ITypeLoader extraClassPathLoader = null; + + public static void setExtraClassPathLoader(ITypeLoader loader){ + extraClassPathLoader = loader; + } + + private final ITypeLoader classpathLoader = new ClasspathTypeLoader(); + + @Override + protected byte[] doLoad(String className) { + Buffer parentBuf = new Buffer(); + if (classpathLoader.tryLoadType(className, parentBuf)) { + return parentBuf.array(); + } + if (extraClassPathLoader != null){ + parentBuf.reset(); + if (extraClassPathLoader.tryLoadType(className, parentBuf)){ + return parentBuf.array(); + } + } + return EMPTY_ARRAY;//need to return *something* as null means no store + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingTypeLoader.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingTypeLoader.java new file mode 100644 index 0000000..5be5ddd --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingTypeLoader.java @@ -0,0 +1,38 @@ +package cuchaz.enigma.source.procyon.typeloader; + +import com.google.common.collect.Maps; +import com.strobel.assembler.metadata.Buffer; +import com.strobel.assembler.metadata.ITypeLoader; + +import java.util.Map; + +/** + * Common cache functions + */ +public abstract class CachingTypeLoader implements ITypeLoader { + protected static final byte[] EMPTY_ARRAY = {}; + + private final Map cache = Maps.newHashMap(); + + protected abstract byte[] doLoad(String className); + + @Override + public boolean tryLoadType(String className, Buffer out) { + + // check the cache + byte[] data = this.cache.computeIfAbsent(className, this::doLoad); + + if (data == EMPTY_ARRAY) { + return false; + } + + out.reset(data.length); + System.arraycopy(data, 0, out.array(), out.position(), data.length); + out.position(0); + return true; + } + + public void clearCache() { + this.cache.clear(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CompiledSourceTypeLoader.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CompiledSourceTypeLoader.java new file mode 100644 index 0000000..e703d3b --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CompiledSourceTypeLoader.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.source.procyon.typeloader; + +import com.google.common.collect.Lists; +import com.strobel.assembler.metadata.Buffer; +import com.strobel.assembler.metadata.ITypeLoader; +import cuchaz.enigma.ClassProvider; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Function; + +public class CompiledSourceTypeLoader extends CachingTypeLoader { + //Store one instance as the classpath shouldn't change during load + private static final ITypeLoader CLASSPATH_TYPE_LOADER = new CachingClasspathTypeLoader(); + + private final ClassProvider compiledSource; + private final LinkedList> visitors = new LinkedList<>(); + + public CompiledSourceTypeLoader(ClassProvider compiledSource) { + this.compiledSource = compiledSource; + } + + public void addVisitor(Function visitor) { + this.visitors.addFirst(visitor); + } + + @Override + protected byte[] doLoad(String className) { + byte[] data = loadType(className); + if (data == null) { + return loadClasspath(className); + } + + return data; + } + + private byte[] loadClasspath(String name) { + Buffer parentBuf = new Buffer(); + if (CLASSPATH_TYPE_LOADER.tryLoadType(name, parentBuf)) { + return parentBuf.array(); + } + return EMPTY_ARRAY; + } + + private byte[] loadType(String className) { + ClassEntry entry = new ClassEntry(className); + + // find the class in the jar + ClassNode node = findClassNode(entry); + if (node == null) { + // couldn't find it + return null; + } + + removeRedundantClassCalls(node); + + ClassWriter writer = new ClassWriter(0); + + ClassVisitor visitor = writer; + for (Function visitorFunction : this.visitors) { + visitor = visitorFunction.apply(visitor); + } + + node.accept(visitor); + + // we have a transformed class! + return writer.toByteArray(); + } + + private void removeRedundantClassCalls(ClassNode node) { + // remove .getClass() calls that are seemingly injected + // DUP + // INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class; + // POP + for (MethodNode methodNode : node.methods) { + AbstractInsnNode insnNode = methodNode.instructions.getFirst(); + while (insnNode != null) { + if (insnNode instanceof MethodInsnNode && insnNode.getOpcode() == Opcodes.INVOKEVIRTUAL) { + MethodInsnNode methodInsnNode = (MethodInsnNode) insnNode; + if (methodInsnNode.name.equals("getClass") && methodInsnNode.owner.equals("java/lang/Object") && methodInsnNode.desc.equals("()Ljava/lang/Class;")) { + AbstractInsnNode previous = methodInsnNode.getPrevious(); + AbstractInsnNode next = methodInsnNode.getNext(); + if (previous.getOpcode() == Opcodes.DUP && next.getOpcode() == Opcodes.POP) { + insnNode = previous.getPrevious();//reset the iterator so it gets the new next instruction + methodNode.instructions.remove(previous); + methodNode.instructions.remove(methodInsnNode); + methodNode.instructions.remove(next); + } + } + } + insnNode = insnNode.getNext(); + } + } + } + + private ClassNode findClassNode(ClassEntry entry) { + // try to find the class in the jar + for (String className : getClassNamesToTry(entry)) { + ClassNode node = compiledSource.getClassNode(className); + if (node != null) { + return node; + } + } + + // didn't find it ;_; + return null; + } + + private Collection getClassNamesToTry(ClassEntry entry) { + List classNamesToTry = Lists.newArrayList(); + classNamesToTry.add(entry.getFullName()); + + ClassEntry outerClass = entry.getOuterClass(); + if (outerClass != null) { + classNamesToTry.addAll(getClassNamesToTry(outerClass)); + } + + return classNamesToTry; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/NoRetryMetadataSystem.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/NoRetryMetadataSystem.java new file mode 100644 index 0000000..c4732b0 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/NoRetryMetadataSystem.java @@ -0,0 +1,38 @@ +package cuchaz.enigma.source.procyon.typeloader; + +import com.strobel.assembler.metadata.ITypeLoader; +import com.strobel.assembler.metadata.MetadataSystem; +import com.strobel.assembler.metadata.TypeDefinition; +import com.strobel.assembler.metadata.TypeReference; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class NoRetryMetadataSystem extends MetadataSystem { + private final Set failedTypes = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + public NoRetryMetadataSystem(final ITypeLoader typeLoader) { + super(typeLoader); + } + + @Override + protected synchronized TypeDefinition resolveType(final String descriptor, final boolean mightBePrimitive) { + if (failedTypes.contains(descriptor)) { + return null; + } + + final TypeDefinition result = super.resolveType(descriptor, mightBePrimitive); + + if (result == null) { + failedTypes.add(descriptor); + } + + return result; + } + + @Override + public synchronized TypeDefinition resolve(final TypeReference type) { + return super.resolve(type); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/SynchronizedTypeLoader.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/SynchronizedTypeLoader.java new file mode 100644 index 0000000..86c6ecc --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/SynchronizedTypeLoader.java @@ -0,0 +1,20 @@ +package cuchaz.enigma.source.procyon.typeloader; + +import com.strobel.assembler.metadata.Buffer; +import com.strobel.assembler.metadata.ITypeLoader; + +/** + * Typeloader with synchronized tryLoadType method + */ +public class SynchronizedTypeLoader implements ITypeLoader { + private final ITypeLoader delegate; + + public SynchronizedTypeLoader(ITypeLoader delegate) { + this.delegate = delegate; + } + + @Override + public synchronized boolean tryLoadType(String internalName, Buffer buffer) { + return delegate.tryLoadType(internalName, buffer); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java b/enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java new file mode 100644 index 0000000..18c966c --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java @@ -0,0 +1,44 @@ +package cuchaz.enigma.translation; + +import cuchaz.enigma.translation.mapping.NameValidator; +import cuchaz.enigma.translation.representation.TypeDescriptor; + +import java.util.Collection; +import java.util.Locale; + +public class LocalNameGenerator { + public static String generateArgumentName(int index, TypeDescriptor desc, Collection arguments) { + boolean uniqueType = arguments.stream().filter(desc::equals).count() <= 1; + String translatedName; + int nameIndex = index + 1; + StringBuilder nameBuilder = new StringBuilder(getTypeName(desc)); + if (!uniqueType || NameValidator.isReserved(nameBuilder.toString())) { + nameBuilder.append(nameIndex); + } + translatedName = nameBuilder.toString(); + return translatedName; + } + + public static String generateLocalVariableName(int index, TypeDescriptor desc) { + int nameIndex = index + 1; + return getTypeName(desc) + nameIndex; + } + + private static String getTypeName(TypeDescriptor desc) { + // Unfortunately each of these have different name getters, so they have different code paths + if (desc.isPrimitive()) { + TypeDescriptor.Primitive argCls = desc.getPrimitive(); + return argCls.name().toLowerCase(Locale.ROOT); + } else if (desc.isArray()) { + // List types would require this whole block again, so just go with aListx + return "arr"; + } else if (desc.isType()) { + String typeName = desc.getTypeEntry().getSimpleName().replace("$", ""); + typeName = typeName.substring(0, 1).toLowerCase(Locale.ROOT) + typeName.substring(1); + return typeName; + } else { + System.err.println("Encountered invalid argument type descriptor " + desc.toString()); + return "var"; + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/MappingTranslator.java b/enigma/src/main/java/cuchaz/enigma/translation/MappingTranslator.java new file mode 100644 index 0000000..529d0ed --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/MappingTranslator.java @@ -0,0 +1,24 @@ +package cuchaz.enigma.translation; + +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.EntryMap; + +public class MappingTranslator implements Translator { + private final EntryMap mappings; + private final EntryResolver resolver; + + public MappingTranslator(EntryMap mappings, EntryResolver resolver) { + this.mappings = mappings; + this.resolver = resolver; + } + + @SuppressWarnings("unchecked") + @Override + public T translate(T translatable) { + if (translatable == null) { + return null; + } + return (T) translatable.translate(this, resolver, mappings); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/ProposingTranslator.java b/enigma/src/main/java/cuchaz/enigma/translation/ProposingTranslator.java new file mode 100644 index 0000000..97866e9 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/ProposingTranslator.java @@ -0,0 +1,51 @@ +package cuchaz.enigma.translation; + +import cuchaz.enigma.api.service.NameProposalService; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.util.Arrays; +import java.util.Optional; + +public class ProposingTranslator implements Translator { + private final EntryRemapper mapper; + private final NameProposalService[] nameProposalServices; + + public ProposingTranslator(EntryRemapper mapper, NameProposalService[] nameProposalServices) { + this.mapper = mapper; + this.nameProposalServices = nameProposalServices; + } + + @Override + @SuppressWarnings("unchecked") + public T translate(T translatable) { + if (translatable == null) { + return null; + } + + T deobfuscated = mapper.deobfuscate(translatable); + + if (translatable instanceof Entry && ((Entry) deobfuscated).getName().equals(((Entry) translatable).getName())) { + return mapper.getObfResolver() + .resolveEntry((Entry) translatable, ResolutionStrategy.RESOLVE_ROOT) + .stream() + .map(this::proposeName) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .map(newName -> (T) ((Entry) deobfuscated).withName(newName)) + .orElse(deobfuscated); + } + + return deobfuscated; + } + + private Optional proposeName(Entry entry) { + return Arrays.stream(nameProposalServices) + .map(service -> service.proposeName(entry, mapper)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/SignatureUpdater.java b/enigma/src/main/java/cuchaz/enigma/translation/SignatureUpdater.java new file mode 100644 index 0000000..3783053 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/SignatureUpdater.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation; + +import com.google.common.collect.Lists; + +import java.io.IOException; +import java.io.StringReader; +import java.util.List; + +public class SignatureUpdater { + + public static String update(String signature, ClassNameUpdater updater) { + try { + StringBuilder buf = new StringBuilder(); + + // read the signature character-by-character + StringReader reader = new StringReader(signature); + int i; + while ((i = reader.read()) != -1) { + char c = (char) i; + + // does this character start a class name? + if (c == 'L') { + // update the class name and add it to the buffer + buf.append('L'); + String className = readClass(reader); + if (className == null) { + throw new IllegalArgumentException("Malformed signature: " + signature); + } + buf.append(updater.update(className)); + buf.append(';'); + } else { + // copy the character into the buffer + buf.append(c); + } + } + + return buf.toString(); + } catch (IOException ex) { + // I'm pretty sure a StringReader will never throw one of these + throw new Error(ex); + } + } + + private static String readClass(StringReader reader) throws IOException { + // read all the characters in the buffer until we hit a ';' + // remember to treat generics correctly + StringBuilder buf = new StringBuilder(); + int depth = 0; + int i; + while ((i = reader.read()) != -1) { + char c = (char) i; + + if (c == '<') { + depth++; + } else if (c == '>') { + depth--; + } else if (depth == 0) { + if (c == ';') { + return buf.toString(); + } else { + buf.append(c); + } + } + } + + return null; + } + + public static List getClasses(String signature) { + final List classNames = Lists.newArrayList(); + update(signature, className -> { + classNames.add(className); + return className; + }); + return classNames; + } + + public interface ClassNameUpdater { + String update(String className); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/Translatable.java b/enigma/src/main/java/cuchaz/enigma/translation/Translatable.java new file mode 100644 index 0000000..0370ef1 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/Translatable.java @@ -0,0 +1,9 @@ +package cuchaz.enigma.translation; + +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.EntryMap; + +public interface Translatable { + Translatable translate(Translator translator, EntryResolver resolver, EntryMap mappings); +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/TranslationDirection.java b/enigma/src/main/java/cuchaz/enigma/translation/TranslationDirection.java new file mode 100644 index 0000000..2ecb30b --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/TranslationDirection.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation; + +public enum TranslationDirection { + + DEOBFUSCATING { + @Override + public T choose(T deobfChoice, T obfChoice) { + if (deobfChoice == null) { + return obfChoice; + } + return deobfChoice; + } + }, + OBFUSCATING { + @Override + public T choose(T deobfChoice, T obfChoice) { + if (obfChoice == null) { + return deobfChoice; + } + return obfChoice; + } + }; + + public abstract T choose(T deobfChoice, T obfChoice); +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/Translator.java b/enigma/src/main/java/cuchaz/enigma/translation/Translator.java new file mode 100644 index 0000000..c70141f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/Translator.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public interface Translator { + T translate(T translatable); + + default Collection translate(Collection translatable) { + return translatable.stream() + .map(this::translate) + .collect(Collectors.toList()); + } + + default Set translate(Set translatable) { + return translatable.stream() + .map(this::translate) + .collect(Collectors.toSet()); + } + + default Map translateKeys(Map translatable) { + Map result = new HashMap<>(translatable.size()); + for (Map.Entry entry : translatable.entrySet()) { + result.put(translate(entry.getKey()), entry.getValue()); + } + return result; + } + + default Map translate(Map translatable) { + Map result = new HashMap<>(translatable.size()); + for (Map.Entry entry : translatable.entrySet()) { + result.put(translate(entry.getKey()), translate(entry.getValue())); + } + return result; + } + + default Multimap translate(Multimap translatable) { + Multimap result = HashMultimap.create(translatable.size(), 1); + for (Map.Entry> entry : translatable.asMap().entrySet()) { + result.putAll(translate(entry.getKey()), translate(entry.getValue())); + } + return result; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/VoidTranslator.java b/enigma/src/main/java/cuchaz/enigma/translation/VoidTranslator.java new file mode 100644 index 0000000..c010833 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/VoidTranslator.java @@ -0,0 +1,10 @@ +package cuchaz.enigma.translation; + +public enum VoidTranslator implements Translator { + INSTANCE; + + @Override + public T translate(T translatable) { + return translatable; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/AccessModifier.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/AccessModifier.java new file mode 100644 index 0000000..5b79b79 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/AccessModifier.java @@ -0,0 +1,25 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.translation.representation.AccessFlags; + +public enum AccessModifier { + UNCHANGED, PUBLIC, PROTECTED, PRIVATE; + + public String getFormattedName() { + return "ACC:" + super.toString(); + } + + public AccessFlags transform(AccessFlags access) { + switch (this) { + case PUBLIC: + return access.setPublic(); + case PROTECTED: + return access.setProtected(); + case PRIVATE: + return access.setPrivate(); + case UNCHANGED: + default: + return access; + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryMap.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryMap.java new file mode 100644 index 0000000..e1a3253 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryMap.java @@ -0,0 +1,24 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.stream.Stream; + +public interface EntryMap { + void insert(Entry entry, T value); + + @Nullable + T remove(Entry entry); + + @Nullable + T get(Entry entry); + + default boolean contains(Entry entry) { + return get(entry) != null; + } + + Stream> getAllEntries(); + + boolean isEmpty(); +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryMapping.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryMapping.java new file mode 100644 index 0000000..c607817 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryMapping.java @@ -0,0 +1,75 @@ +package cuchaz.enigma.translation.mapping; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class EntryMapping { + private final String targetName; + private final AccessModifier accessModifier; + private final @Nullable String javadoc; + + public EntryMapping(@Nonnull String targetName) { + this(targetName, AccessModifier.UNCHANGED); + } + + public EntryMapping(@Nonnull String targetName, @Nullable String javadoc) { + this(targetName, AccessModifier.UNCHANGED, javadoc); + } + + public EntryMapping(@Nonnull String targetName, AccessModifier accessModifier) { + this(targetName, accessModifier, null); + } + + public EntryMapping(@Nonnull String targetName, AccessModifier accessModifier, @Nullable String javadoc) { + this.targetName = targetName; + this.accessModifier = accessModifier; + this.javadoc = javadoc; + } + + @Nonnull + public String getTargetName() { + return targetName; + } + + @Nonnull + public AccessModifier getAccessModifier() { + if (accessModifier == null) { + return AccessModifier.UNCHANGED; + } + return accessModifier; + } + + @Nullable + public String getJavadoc() { + return javadoc; + } + + public EntryMapping withName(String newName) { + return new EntryMapping(newName, accessModifier, javadoc); + } + + public EntryMapping withModifier(AccessModifier newModifier) { + return new EntryMapping(targetName, newModifier, javadoc); + } + + public EntryMapping withDocs(String newDocs) { + return new EntryMapping(targetName, accessModifier, newDocs); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + + if (obj instanceof EntryMapping) { + EntryMapping mapping = (EntryMapping) obj; + return mapping.targetName.equals(targetName) && mapping.accessModifier.equals(accessModifier); + } + + return false; + } + + @Override + public int hashCode() { + return targetName.hashCode() + accessModifier.hashCode() * 31; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java new file mode 100644 index 0000000..1dd7eac --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java @@ -0,0 +1,104 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.MappingTranslator; +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.stream.Stream; + +public class EntryRemapper { + private final DeltaTrackingTree obfToDeobf; + + private final EntryResolver obfResolver; + private final Translator deobfuscator; + + private final MappingValidator validator; + + private EntryRemapper(JarIndex jarIndex, EntryTree obfToDeobf) { + this.obfToDeobf = new DeltaTrackingTree<>(obfToDeobf); + + this.obfResolver = jarIndex.getEntryResolver(); + + this.deobfuscator = new MappingTranslator(obfToDeobf, obfResolver); + + this.validator = new MappingValidator(obfToDeobf, deobfuscator, jarIndex); + } + + public static EntryRemapper mapped(JarIndex index, EntryTree obfToDeobf) { + return new EntryRemapper(index, obfToDeobf); + } + + public static EntryRemapper empty(JarIndex index) { + return new EntryRemapper(index, new HashEntryTree<>()); + } + + public > void mapFromObf(E obfuscatedEntry, @Nullable EntryMapping deobfMapping) { + mapFromObf(obfuscatedEntry, deobfMapping, true); + } + + public > void mapFromObf(E obfuscatedEntry, @Nullable EntryMapping deobfMapping, boolean renaming) { + Collection resolvedEntries = obfResolver.resolveEntry(obfuscatedEntry, renaming ? ResolutionStrategy.RESOLVE_ROOT : ResolutionStrategy.RESOLVE_CLOSEST); + + if (renaming && deobfMapping != null) { + for (E resolvedEntry : resolvedEntries) { + validator.validateRename(resolvedEntry, deobfMapping.getTargetName()); + } + } + + for (E resolvedEntry : resolvedEntries) { + obfToDeobf.insert(resolvedEntry, deobfMapping); + } + } + + public void removeByObf(Entry obfuscatedEntry) { + mapFromObf(obfuscatedEntry, null); + } + + @Nullable + public EntryMapping getDeobfMapping(Entry entry) { + return obfToDeobf.get(entry); + } + + public boolean hasDeobfMapping(Entry obfEntry) { + return obfToDeobf.contains(obfEntry); + } + + public T deobfuscate(T translatable) { + return deobfuscator.translate(translatable); + } + + public Translator getDeobfuscator() { + return deobfuscator; + } + + public Stream> getObfEntries() { + return obfToDeobf.getAllEntries(); + } + + public Collection> getObfChildren(Entry obfuscatedEntry) { + return obfToDeobf.getChildren(obfuscatedEntry); + } + + public DeltaTrackingTree getObfToDeobf() { + return obfToDeobf; + } + + public MappingDelta takeMappingDelta() { + return obfToDeobf.takeDelta(); + } + + public boolean isDirty() { + return obfToDeobf.isDirty(); + } + + public EntryResolver getObfResolver() { + return obfResolver; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryResolver.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryResolver.java new file mode 100644 index 0000000..521f72d --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryResolver.java @@ -0,0 +1,41 @@ +package cuchaz.enigma.translation.mapping; + +import com.google.common.collect.Streams; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +public interface EntryResolver { + > Collection resolveEntry(E entry, ResolutionStrategy strategy); + + default > E resolveFirstEntry(E entry, ResolutionStrategy strategy) { + return resolveEntry(entry, strategy).stream().findFirst().orElse(entry); + } + + default , C extends Entry> Collection> resolveReference(EntryReference reference, ResolutionStrategy strategy) { + Collection entry = resolveEntry(reference.entry, strategy); + if (reference.context != null) { + Collection context = resolveEntry(reference.context, strategy); + return Streams.zip(entry.stream(), context.stream(), (e, c) -> new EntryReference<>(e, c, reference)) + .collect(Collectors.toList()); + } else { + return entry.stream() + .map(e -> new EntryReference<>(e, null, reference)) + .collect(Collectors.toList()); + } + } + + default , C extends Entry> EntryReference resolveFirstReference(EntryReference reference, ResolutionStrategy strategy) { + E entry = resolveFirstEntry(reference.entry, strategy); + C context = resolveFirstEntry(reference.context, strategy); + return new EntryReference<>(entry, context, reference); + } + + Set> resolveEquivalentEntries(Entry entry); + + Set resolveEquivalentMethods(MethodEntry methodEntry); +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java new file mode 100644 index 0000000..a7f83cd --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.mapping; + +public class IllegalNameException extends RuntimeException { + + private String name; + private String reason; + + public IllegalNameException(String name, String reason) { + this.name = name; + this.reason = reason; + } + + public String getReason() { + return this.reason; + } + + @Override + public String getMessage() { + StringBuilder buf = new StringBuilder(); + buf.append("Illegal name: "); + buf.append(this.name); + if (this.reason != null) { + buf.append(" because "); + buf.append(this.reason); + } + return buf.toString(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/IndexEntryResolver.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/IndexEntryResolver.java new file mode 100644 index 0000000..78231dd --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/IndexEntryResolver.java @@ -0,0 +1,227 @@ +package cuchaz.enigma.translation.mapping; + +import com.google.common.collect.Sets; +import cuchaz.enigma.analysis.IndexTreeBuilder; +import cuchaz.enigma.analysis.MethodImplementationsTreeNode; +import cuchaz.enigma.analysis.MethodInheritanceTreeNode; +import cuchaz.enigma.analysis.index.BridgeMethodIndex; +import cuchaz.enigma.analysis.index.EntryIndex; +import cuchaz.enigma.analysis.index.InheritanceIndex; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.VoidTranslator; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; + +public class IndexEntryResolver implements EntryResolver { + private final EntryIndex entryIndex; + private final InheritanceIndex inheritanceIndex; + private final BridgeMethodIndex bridgeMethodIndex; + + private final IndexTreeBuilder treeBuilder; + + public IndexEntryResolver(JarIndex index) { + this.entryIndex = index.getEntryIndex(); + this.inheritanceIndex = index.getInheritanceIndex(); + this.bridgeMethodIndex = index.getBridgeMethodIndex(); + + this.treeBuilder = new IndexTreeBuilder(index); + } + + @Override + @SuppressWarnings("unchecked") + public > Collection resolveEntry(E entry, ResolutionStrategy strategy) { + if (entry == null) { + return Collections.emptySet(); + } + + Entry classChild = getClassChild(entry); + if (classChild != null && !(classChild instanceof ClassEntry)) { + AccessFlags access = entryIndex.getEntryAccess(classChild); + + // If we're looking for the closest and this entry exists, we're done looking + if (strategy == ResolutionStrategy.RESOLVE_CLOSEST && access != null) { + return Collections.singleton(entry); + } + + if (access == null || !access.isPrivate()) { + Collection> resolvedChildren = resolveChildEntry(classChild, strategy); + if (!resolvedChildren.isEmpty()) { + return resolvedChildren.stream() + .map(resolvedChild -> (E) entry.replaceAncestor(classChild, resolvedChild)) + .collect(Collectors.toList()); + } + } + } + + return Collections.singleton(entry); + } + + @Nullable + private Entry getClassChild(Entry entry) { + if (entry instanceof ClassEntry) { + return null; + } + + // get the entry in the hierarchy that is the child of a class + List> ancestry = entry.getAncestry(); + for (int i = ancestry.size() - 1; i > 0; i--) { + Entry child = ancestry.get(i); + Entry cast = child.castParent(ClassEntry.class); + if (cast != null && !(cast instanceof ClassEntry)) { + // we found the entry which is a child of a class, we are now able to resolve the owner of this entry + return cast; + } + } + + return null; + } + + private Set> resolveChildEntry(Entry entry, ResolutionStrategy strategy) { + ClassEntry ownerClass = entry.getParent(); + + if (entry instanceof MethodEntry) { + MethodEntry bridgeMethod = bridgeMethodIndex.getBridgeFromSpecialized((MethodEntry) entry); + if (bridgeMethod != null && ownerClass.equals(bridgeMethod.getParent())) { + Set> resolvedBridge = resolveChildEntry(bridgeMethod, strategy); + if (!resolvedBridge.isEmpty()) { + return resolvedBridge; + } else { + return Collections.singleton(bridgeMethod); + } + } + } + + Set> resolvedEntries = new HashSet<>(); + + for (ClassEntry parentClass : inheritanceIndex.getParents(ownerClass)) { + Entry parentEntry = entry.withParent(parentClass); + + if (strategy == ResolutionStrategy.RESOLVE_ROOT) { + resolvedEntries.addAll(resolveRoot(parentEntry, strategy)); + } else { + resolvedEntries.addAll(resolveClosest(parentEntry, strategy)); + } + } + + return resolvedEntries; + } + + private Collection> resolveRoot(Entry entry, ResolutionStrategy strategy) { + // When resolving root, we want to first look for the lowest entry before returning ourselves + Set> parentResolution = resolveChildEntry(entry, strategy); + + if (parentResolution.isEmpty()) { + AccessFlags parentAccess = entryIndex.getEntryAccess(entry); + if (parentAccess != null && !parentAccess.isPrivate()) { + return Collections.singleton(entry); + } + } + + return parentResolution; + } + + private Collection> resolveClosest(Entry entry, ResolutionStrategy strategy) { + // When resolving closest, we want to first check if we exist before looking further down + AccessFlags parentAccess = entryIndex.getEntryAccess(entry); + if (parentAccess != null && !parentAccess.isPrivate()) { + return Collections.singleton(entry); + } else { + return resolveChildEntry(entry, strategy); + } + } + + @Override + public Set> resolveEquivalentEntries(Entry entry) { + MethodEntry relevantMethod = entry.findAncestor(MethodEntry.class); + if (relevantMethod == null || !entryIndex.hasMethod(relevantMethod)) { + return Collections.singleton(entry); + } + + Set equivalentMethods = resolveEquivalentMethods(relevantMethod); + Set> equivalentEntries = new HashSet<>(equivalentMethods.size()); + + for (MethodEntry equivalentMethod : equivalentMethods) { + Entry equivalentEntry = entry.replaceAncestor(relevantMethod, equivalentMethod); + equivalentEntries.add(equivalentEntry); + } + + return equivalentEntries; + } + + @Override + public Set resolveEquivalentMethods(MethodEntry methodEntry) { + AccessFlags access = entryIndex.getMethodAccess(methodEntry); + if (access == null) { + throw new IllegalArgumentException("Could not find method " + methodEntry); + } + + if (!canInherit(methodEntry, access)) { + return Collections.singleton(methodEntry); + } + + Set methodEntries = Sets.newHashSet(); + resolveEquivalentMethods(methodEntries, treeBuilder.buildMethodInheritance(VoidTranslator.INSTANCE, methodEntry)); + return methodEntries; + } + + private void resolveEquivalentMethods(Set methodEntries, MethodInheritanceTreeNode node) { + MethodEntry methodEntry = node.getMethodEntry(); + if (methodEntries.contains(methodEntry)) { + return; + } + + AccessFlags flags = entryIndex.getMethodAccess(methodEntry); + if (flags != null && canInherit(methodEntry, flags)) { + // collect the entry + methodEntries.add(methodEntry); + } + + // look at bridge methods! + MethodEntry bridgedMethod = bridgeMethodIndex.getBridgeFromSpecialized(methodEntry); + while (bridgedMethod != null) { + methodEntries.addAll(resolveEquivalentMethods(bridgedMethod)); + bridgedMethod = bridgeMethodIndex.getBridgeFromSpecialized(bridgedMethod); + } + + // look at interface methods too + for (MethodImplementationsTreeNode implementationsNode : treeBuilder.buildMethodImplementations(VoidTranslator.INSTANCE, methodEntry)) { + resolveEquivalentMethods(methodEntries, implementationsNode); + } + + // recurse + for (int i = 0; i < node.getChildCount(); i++) { + resolveEquivalentMethods(methodEntries, (MethodInheritanceTreeNode) node.getChildAt(i)); + } + } + + private void resolveEquivalentMethods(Set methodEntries, MethodImplementationsTreeNode node) { + MethodEntry methodEntry = node.getMethodEntry(); + AccessFlags flags = entryIndex.getMethodAccess(methodEntry); + if (flags != null && !flags.isPrivate() && !flags.isStatic()) { + // collect the entry + methodEntries.add(methodEntry); + } + + // look at bridge methods! + MethodEntry bridgedMethod = bridgeMethodIndex.getBridgeFromSpecialized(methodEntry); + while (bridgedMethod != null) { + methodEntries.addAll(resolveEquivalentMethods(bridgedMethod)); + bridgedMethod = bridgeMethodIndex.getBridgeFromSpecialized(bridgedMethod); + } + + // recurse + for (int i = 0; i < node.getChildCount(); i++) { + resolveEquivalentMethods(methodEntries, (MethodImplementationsTreeNode) node.getChildAt(i)); + } + } + + private boolean canInherit(MethodEntry entry, AccessFlags access) { + return !entry.isConstructor() && !access.isPrivate() && !access.isStatic() && !access.isFinal(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingDelta.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingDelta.java new file mode 100644 index 0000000..1407bb6 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingDelta.java @@ -0,0 +1,54 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.util.stream.Stream; + +public class MappingDelta implements Translatable { + public static final Object PLACEHOLDER = new Object(); + + private final EntryTree baseMappings; + + private final EntryTree changes; + + public MappingDelta(EntryTree baseMappings, EntryTree changes) { + this.baseMappings = baseMappings; + this.changes = changes; + } + + public MappingDelta(EntryTree baseMappings) { + this(baseMappings, new HashEntryTree<>()); + } + + public static MappingDelta added(EntryTree mappings) { + EntryTree changes = new HashEntryTree<>(); + mappings.getAllEntries().forEach(entry -> changes.insert(entry, PLACEHOLDER)); + + return new MappingDelta<>(new HashEntryTree<>(), changes); + } + + public EntryTree getBaseMappings() { + return baseMappings; + } + + public EntryTree getChanges() { + return changes; + } + + public Stream> getChangedRoots() { + return changes.getRootNodes().map(EntryTreeNode::getEntry); + } + + @Override + public MappingDelta translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + return new MappingDelta<>( + translator.translate(baseMappings), + translator.translate(changes) + ); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingOperations.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingOperations.java new file mode 100644 index 0000000..2c03748 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingOperations.java @@ -0,0 +1,71 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.MappingTranslator; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.util.HashSet; +import java.util.Set; + +public class MappingOperations { + public static EntryTree invert(EntryTree mappings) { + Translator translator = new MappingTranslator(mappings, VoidEntryResolver.INSTANCE); + EntryTree result = new HashEntryTree<>(); + + for (EntryTreeNode node : mappings) { + Entry leftEntry = node.getEntry(); + EntryMapping leftMapping = node.getValue(); + + if (!(leftEntry instanceof ClassEntry || leftEntry instanceof MethodEntry || leftEntry instanceof FieldEntry)) { + result.insert(translator.translate(leftEntry), leftMapping); + continue; + } + + Entry rightEntry = translator.translate(leftEntry); + + result.insert(rightEntry, leftMapping == null ? null : leftMapping.withName(leftEntry.getName())); + } + + return result; + } + + public static EntryTree compose(EntryTree left, EntryTree right, boolean keepLeftOnly, boolean keepRightOnly) { + Translator leftTranslator = new MappingTranslator(left, VoidEntryResolver.INSTANCE); + EntryTree result = new HashEntryTree<>(); + Set> addedMappings = new HashSet<>(); + + for (EntryTreeNode node : left) { + Entry leftEntry = node.getEntry(); + EntryMapping leftMapping = node.getValue(); + + Entry rightEntry = leftTranslator.translate(leftEntry); + + EntryMapping rightMapping = right.get(rightEntry); + if (rightMapping != null) { + result.insert(leftEntry, rightMapping); + addedMappings.add(rightEntry); + } else if (keepLeftOnly) { + result.insert(leftEntry, leftMapping); + } + } + + if (keepRightOnly) { + Translator leftInverseTranslator = new MappingTranslator(invert(left), VoidEntryResolver.INSTANCE); + for (EntryTreeNode node : right) { + Entry rightEntry = node.getEntry(); + EntryMapping rightMapping = node.getValue(); + + if (!addedMappings.contains(rightEntry)) { + result.insert(leftInverseTranslator.translate(rightEntry), rightMapping); + } + } + } + return result; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingPair.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingPair.java new file mode 100644 index 0000000..5d39e3d --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingPair.java @@ -0,0 +1,32 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; + +public class MappingPair, M> { + private final E entry; + private M mapping; + + public MappingPair(E entry, @Nullable M mapping) { + this.entry = entry; + this.mapping = mapping; + } + + public MappingPair(E entry) { + this(entry, null); + } + + public E getEntry() { + return entry; + } + + @Nullable + public M getMapping() { + return mapping; + } + + public void setMapping(M mapping) { + this.mapping = mapping; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java new file mode 100644 index 0000000..ae615da --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java @@ -0,0 +1,75 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.analysis.index.InheritanceIndex; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.util.Collection; +import java.util.HashSet; +import java.util.stream.Collectors; + +public class MappingValidator { + private final EntryTree obfToDeobf; + private final Translator deobfuscator; + private final JarIndex index; + + public MappingValidator(EntryTree obfToDeobf, Translator deobfuscator, JarIndex index) { + this.obfToDeobf = obfToDeobf; + this.deobfuscator = deobfuscator; + this.index = index; + } + + public void validateRename(Entry entry, String name) throws IllegalNameException { + Collection> equivalentEntries = index.getEntryResolver().resolveEquivalentEntries(entry); + for (Entry equivalentEntry : equivalentEntries) { + equivalentEntry.validateName(name); + validateUnique(equivalentEntry, name); + } + } + + private void validateUnique(Entry entry, String name) { + ClassEntry containingClass = entry.getContainingClass(); + Collection relatedClasses = getRelatedClasses(containingClass); + + for (ClassEntry relatedClass : relatedClasses) { + Entry relatedEntry = entry.replaceAncestor(containingClass, relatedClass); + Entry translatedEntry = deobfuscator.translate(relatedEntry); + + Collection> translatedSiblings = obfToDeobf.getSiblings(relatedEntry).stream() + .map(deobfuscator::translate) + .collect(Collectors.toList()); + + if (!isUnique(translatedEntry, translatedSiblings, name)) { + Entry parent = translatedEntry.getParent(); + if (parent != null) { + throw new IllegalNameException(name, "Name is not unique in " + parent + "!"); + } else { + throw new IllegalNameException(name, "Name is not unique!"); + } + } + } + } + + private Collection getRelatedClasses(ClassEntry classEntry) { + InheritanceIndex inheritanceIndex = index.getInheritanceIndex(); + + Collection relatedClasses = new HashSet<>(); + relatedClasses.add(classEntry); + relatedClasses.addAll(inheritanceIndex.getChildren(classEntry)); + relatedClasses.addAll(inheritanceIndex.getAncestors(classEntry)); + + return relatedClasses; + } + + private boolean isUnique(Entry entry, Collection> siblings, String name) { + for (Entry sibling : siblings) { + if (entry.canConflictWith(sibling) && sibling.getName().equals(name)) { + return false; + } + } + return true; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingsChecker.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingsChecker.java new file mode 100644 index 0000000..5d9794f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingsChecker.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class MappingsChecker { + private final JarIndex index; + private final EntryTree mappings; + + public MappingsChecker(JarIndex index, EntryTree mappings) { + this.index = index; + this.mappings = mappings; + } + + public Dropped dropBrokenMappings(ProgressListener progress) { + Dropped dropped = new Dropped(); + + Collection> obfEntries = mappings.getAllEntries() + .filter(e -> e instanceof ClassEntry || e instanceof MethodEntry || e instanceof FieldEntry || e instanceof LocalVariableEntry) + .collect(Collectors.toList()); + + progress.init(obfEntries.size(), "Checking for dropped mappings"); + + int steps = 0; + for (Entry entry : obfEntries) { + progress.step(steps++, entry.toString()); + tryDropEntry(dropped, entry); + } + + dropped.apply(mappings); + + return dropped; + } + + private void tryDropEntry(Dropped dropped, Entry entry) { + if (shouldDropEntry(entry)) { + EntryMapping mapping = mappings.get(entry); + if (mapping != null) { + dropped.drop(entry, mapping); + } + } + } + + private boolean shouldDropEntry(Entry entry) { + if (!index.getEntryIndex().hasEntry(entry)) { + return true; + } + Collection> resolvedEntries = index.getEntryResolver().resolveEntry(entry, ResolutionStrategy.RESOLVE_ROOT); + return !resolvedEntries.contains(entry); + } + + public static class Dropped { + private final Map, String> droppedMappings = new HashMap<>(); + + public void drop(Entry entry, EntryMapping mapping) { + droppedMappings.put(entry, mapping.getTargetName()); + } + + void apply(EntryTree mappings) { + for (Entry entry : droppedMappings.keySet()) { + EntryTreeNode node = mappings.findNode(entry); + if (node == null) { + continue; + } + + for (Entry childEntry : node.getChildrenRecursively()) { + mappings.remove(childEntry); + } + } + } + + public Map, String> getDroppedMappings() { + return droppedMappings; + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java new file mode 100644 index 0000000..74ba633 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.mapping; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +public class NameValidator { + private static final Pattern IDENTIFIER_PATTERN; + private static final Pattern CLASS_PATTERN; + private static final List ILLEGAL_IDENTIFIERS = Arrays.asList( + "abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized", + "boolean", "do", "if", "private", "this", "break", "double", "implements", "protected", "throw", "byte", + "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch", + "extends", "int", "short", "try", "char", "final", "interface", "static", "void", "class", "finally", + "long", "strictfp", "volatile", "const", "float", "native", "super", "while", "_" + ); + + static { + String identifierRegex = "[A-Za-z_<][A-Za-z0-9_>]*"; + IDENTIFIER_PATTERN = Pattern.compile(identifierRegex); + CLASS_PATTERN = Pattern.compile(String.format("^(%s(\\.|/))*(%s)$", identifierRegex, identifierRegex)); + } + + public static void validateClassName(String name) { + if (!CLASS_PATTERN.matcher(name).matches() || ILLEGAL_IDENTIFIERS.contains(name)) { + throw new IllegalNameException(name, "This doesn't look like a legal class name"); + } + } + + public static void validateIdentifier(String name) { + if (!IDENTIFIER_PATTERN.matcher(name).matches() || ILLEGAL_IDENTIFIERS.contains(name)) { + throw new IllegalNameException(name, "This doesn't look like a legal identifier"); + } + } + + public static boolean isReserved(String name) { + return ILLEGAL_IDENTIFIERS.contains(name); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/ResolutionStrategy.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/ResolutionStrategy.java new file mode 100644 index 0000000..1c28e02 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/ResolutionStrategy.java @@ -0,0 +1,6 @@ +package cuchaz.enigma.translation.mapping; + +public enum ResolutionStrategy { + RESOLVE_ROOT, + RESOLVE_CLOSEST +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/VoidEntryResolver.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/VoidEntryResolver.java new file mode 100644 index 0000000..2eab55f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/VoidEntryResolver.java @@ -0,0 +1,27 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +public enum VoidEntryResolver implements EntryResolver { + INSTANCE; + + @Override + public > Collection resolveEntry(E entry, ResolutionStrategy strategy) { + return Collections.singleton(entry); + } + + @Override + public Set> resolveEquivalentEntries(Entry entry) { + return Collections.singleton(entry); + } + + @Override + public Set resolveEquivalentMethods(MethodEntry methodEntry) { + return Collections.singleton(methodEntry); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/LfPrintWriter.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/LfPrintWriter.java new file mode 100644 index 0000000..441949c --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/LfPrintWriter.java @@ -0,0 +1,16 @@ +package cuchaz.enigma.translation.mapping.serde; + +import java.io.PrintWriter; +import java.io.Writer; + +public class LfPrintWriter extends PrintWriter { + public LfPrintWriter(Writer out) { + super(out); + } + + @Override + public void println() { + // https://stackoverflow.com/a/14749004 + write('\n'); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFileNameFormat.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFileNameFormat.java new file mode 100644 index 0000000..9bc013c --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFileNameFormat.java @@ -0,0 +1,10 @@ +package cuchaz.enigma.translation.mapping.serde; + +import com.google.gson.annotations.SerializedName; + +public enum MappingFileNameFormat { + @SerializedName("by_obf") + BY_OBF, + @SerializedName("by_deobf") + BY_DEOBF +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFormat.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFormat.java new file mode 100644 index 0000000..ca275eb --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingFormat.java @@ -0,0 +1,65 @@ +package cuchaz.enigma.translation.mapping.serde; + +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingDelta; +import cuchaz.enigma.translation.mapping.serde.enigma.EnigmaMappingsReader; +import cuchaz.enigma.translation.mapping.serde.enigma.EnigmaMappingsWriter; +import cuchaz.enigma.translation.mapping.serde.proguard.ProguardMappingsReader; +import cuchaz.enigma.translation.mapping.serde.srg.SrgMappingsWriter; +import cuchaz.enigma.translation.mapping.serde.tiny.TinyMappingsReader; +import cuchaz.enigma.translation.mapping.serde.tiny.TinyMappingsWriter; +import cuchaz.enigma.translation.mapping.serde.tinyv2.TinyV2Reader; +import cuchaz.enigma.translation.mapping.serde.tinyv2.TinyV2Writer; +import cuchaz.enigma.translation.mapping.tree.EntryTree; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.Path; + +public enum MappingFormat { + ENIGMA_FILE(EnigmaMappingsWriter.FILE, EnigmaMappingsReader.FILE), + ENIGMA_DIRECTORY(EnigmaMappingsWriter.DIRECTORY, EnigmaMappingsReader.DIRECTORY), + ENIGMA_ZIP(EnigmaMappingsWriter.ZIP, EnigmaMappingsReader.ZIP), + TINY_V2(new TinyV2Writer("intermediary", "named"), new TinyV2Reader()), + TINY_FILE(TinyMappingsWriter.INSTANCE, TinyMappingsReader.INSTANCE), + SRG_FILE(SrgMappingsWriter.INSTANCE, null), + PROGUARD(null, ProguardMappingsReader.INSTANCE); + + + private final MappingsWriter writer; + private final MappingsReader reader; + + MappingFormat(MappingsWriter writer, MappingsReader reader) { + this.writer = writer; + this.reader = reader; + } + + public void write(EntryTree mappings, Path path, ProgressListener progressListener, MappingSaveParameters saveParameters) { + write(mappings, MappingDelta.added(mappings), path, progressListener, saveParameters); + } + + public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progressListener, MappingSaveParameters saveParameters) { + if (writer == null) { + throw new IllegalStateException(name() + " does not support writing"); + } + writer.write(mappings, delta, path, progressListener, saveParameters); + } + + public EntryTree read(Path path, ProgressListener progressListener, MappingSaveParameters saveParameters) throws IOException, MappingParseException { + if (reader == null) { + throw new IllegalStateException(name() + " does not support reading"); + } + return reader.read(path, progressListener, saveParameters); + } + + @Nullable + public MappingsWriter getWriter() { + return writer; + } + + @Nullable + public MappingsReader getReader() { + return reader; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingHelper.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingHelper.java new file mode 100644 index 0000000..7c8f6cc --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingHelper.java @@ -0,0 +1,51 @@ +package cuchaz.enigma.translation.mapping.serde; + +public final class MappingHelper { + private static final String TO_ESCAPE = "\\\n\r\0\t"; + private static final String ESCAPED = "\\nr0t"; + + public static String escape(String raw) { + StringBuilder builder = new StringBuilder(raw.length() + 1); + for (int i = 0; i < raw.length(); i++) { + final char c = raw.charAt(i); + final int r = TO_ESCAPE.indexOf(c); + if (r < 0) { + builder.append(c); + } else { + builder.append('\\').append(ESCAPED.charAt(r)); + } + } + return builder.toString(); + } + + public static String unescape(String str) { + int pos = str.indexOf('\\'); + if (pos < 0) return str; + + StringBuilder ret = new StringBuilder(str.length() - 1); + int start = 0; + + do { + ret.append(str, start, pos); + pos++; + int type; + + if (pos >= str.length()) { + throw new RuntimeException("incomplete escape sequence at the end"); + } else if ((type = ESCAPED.indexOf(str.charAt(pos))) < 0) { + throw new RuntimeException("invalid escape character: \\" + str.charAt(pos)); + } else { + ret.append(TO_ESCAPE.charAt(type)); + } + + start = pos + 1; + } while ((pos = str.indexOf('\\', start)) >= 0); + + ret.append(str, start, str.length()); + + return ret.toString(); + } + + private MappingHelper() { + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingParseException.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingParseException.java new file mode 100644 index 0000000..9d04b97 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingParseException.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.mapping.serde; + +import java.io.File; +import java.util.function.Supplier; + +public class MappingParseException extends Exception { + + private int line; + private String message; + private String filePath; + + public MappingParseException(File file, int line, String message) { + this.line = line; + this.message = message; + filePath = file.getAbsolutePath(); + } + + public MappingParseException(Supplier filenameProvider, int line, String message) { + this.line = line; + this.message = message; + filePath = filenameProvider.get(); + } + + @Override + public String getMessage() { + return "Line " + line + ": " + message + " in file " + filePath; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingSaveParameters.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingSaveParameters.java new file mode 100644 index 0000000..e78fd95 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingSaveParameters.java @@ -0,0 +1,16 @@ +package cuchaz.enigma.translation.mapping.serde; + +import com.google.gson.annotations.SerializedName; + +public class MappingSaveParameters { + @SerializedName("file_name_format") + private final MappingFileNameFormat fileNameFormat; + + public MappingSaveParameters(MappingFileNameFormat fileNameFormat) { + this.fileNameFormat = fileNameFormat; + } + + public MappingFileNameFormat getFileNameFormat() { + return fileNameFormat; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsReader.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsReader.java new file mode 100644 index 0000000..2f01375 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsReader.java @@ -0,0 +1,12 @@ +package cuchaz.enigma.translation.mapping.serde; + +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.tree.EntryTree; + +import java.io.IOException; +import java.nio.file.Path; + +public interface MappingsReader { + EntryTree read(Path path, ProgressListener progress, MappingSaveParameters saveParameters) throws MappingParseException, IOException; +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsWriter.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsWriter.java new file mode 100644 index 0000000..68a8dbb --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/MappingsWriter.java @@ -0,0 +1,16 @@ +package cuchaz.enigma.translation.mapping.serde; + +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingDelta; +import cuchaz.enigma.translation.mapping.tree.EntryTree; + +import java.nio.file.Path; + +public interface MappingsWriter { + void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress, MappingSaveParameters saveParameters); + + default void write(EntryTree mappings, Path path, ProgressListener progress, MappingSaveParameters saveParameters) { + write(mappings, MappingDelta.added(mappings), path, progress, saveParameters); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/RawEntryMapping.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/RawEntryMapping.java new file mode 100644 index 0000000..79587a0 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/RawEntryMapping.java @@ -0,0 +1,30 @@ +package cuchaz.enigma.translation.mapping.serde; + +import cuchaz.enigma.translation.mapping.AccessModifier; +import cuchaz.enigma.translation.mapping.EntryMapping; + +import java.util.ArrayList; +import java.util.List; + +public final class RawEntryMapping { + private final String targetName; + private final AccessModifier access; + private List javadocs = new ArrayList<>(); + + public RawEntryMapping(String targetName) { + this(targetName, null); + } + + public RawEntryMapping(String targetName, AccessModifier access) { + this.access = access; + this.targetName = targetName; + } + + public void addJavadocLine(String line) { + javadocs.add(line); + } + + public EntryMapping bake() { + return new EntryMapping(targetName, access, javadocs.isEmpty() ? null : String.join("\n", javadocs)); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/enigma/EnigmaFormat.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/enigma/EnigmaFormat.java new file mode 100644 index 0000000..210d328 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/enigma/EnigmaFormat.java @@ -0,0 +1,9 @@ +package cuchaz.enigma.translation.mapping.serde.enigma; + +public class EnigmaFormat { + public static final String COMMENT = "COMMENT"; + public static final String CLASS = "CLASS"; + public static final String FIELD = "FIELD"; + public static final String METHOD = "METHOD"; + public static final String PARAMETER = "ARG"; +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/enigma/EnigmaMappingsReader.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/enigma/EnigmaMappingsReader.java new file mode 100644 index 0000000..30de85f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/enigma/EnigmaMappingsReader.java @@ -0,0 +1,322 @@ +package cuchaz.enigma.translation.mapping.serde.enigma; + +import com.google.common.base.Charsets; +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.mapping.serde.MappingParseException; +import cuchaz.enigma.translation.mapping.AccessModifier; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingPair; +import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; +import cuchaz.enigma.translation.mapping.serde.MappingHelper; +import cuchaz.enigma.translation.mapping.serde.MappingsReader; +import cuchaz.enigma.translation.mapping.serde.RawEntryMapping; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.*; +import cuchaz.enigma.utils.I18n; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +public enum EnigmaMappingsReader implements MappingsReader { + FILE { + @Override + public EntryTree read(Path path, ProgressListener progress, MappingSaveParameters saveParameters) throws IOException, MappingParseException { + progress.init(1, I18n.translate("progress.mappings.enigma_file.loading")); + + EntryTree mappings = new HashEntryTree<>(); + readFile(path, mappings); + + progress.step(1, I18n.translate("progress.mappings.enigma_file.done")); + + return mappings; + } + }, + DIRECTORY { + @Override + public EntryTree read(Path root, ProgressListener progress, MappingSaveParameters saveParameters) throws IOException, MappingParseException { + EntryTree mappings = new HashEntryTree<>(); + + List files = Files.walk(root) + .filter(f -> !Files.isDirectory(f)) + .filter(f -> f.toString().endsWith(".mapping")) + .collect(Collectors.toList()); + + progress.init(files.size(), I18n.translate("progress.mappings.enigma_directory.loading")); + int step = 0; + + for (Path file : files) { + progress.step(step++, root.relativize(file).toString()); + if (Files.isHidden(file)) { + continue; + } + readFile(file, mappings); + } + + return mappings; + } + }, + ZIP { + @Override + public EntryTree read(Path zip, ProgressListener progress, MappingSaveParameters saveParameters) throws MappingParseException, IOException { + try (FileSystem fs = FileSystems.newFileSystem(zip, (ClassLoader) null)) { + return DIRECTORY.read(fs.getPath("/"), progress, saveParameters); + } + } + }; + + protected void readFile(Path path, EntryTree mappings) throws IOException, MappingParseException { + List lines = Files.readAllLines(path, Charsets.UTF_8); + Deque> mappingStack = new ArrayDeque<>(); + + for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { + String line = lines.get(lineNumber); + int indentation = countIndentation(line); + + line = formatLine(line); + if (line == null) { + continue; + } + + cleanMappingStack(indentation, mappingStack, mappings); + + try { + MappingPair pair = parseLine(mappingStack.peek(), line); + if (pair != null) { + mappingStack.push(pair); + if (pair.getMapping() != null) { + + } + } + } catch (Throwable t) { + t.printStackTrace(); + throw new MappingParseException(path::toString, lineNumber, t.toString()); + } + } + + // Clean up rest + cleanMappingStack(0, mappingStack, mappings); + } + + private void cleanMappingStack(int indentation, Deque> mappingStack, EntryTree mappings) { + while (indentation < mappingStack.size()) { + MappingPair pair = mappingStack.pop(); + if (pair.getMapping() != null) { + mappings.insert(pair.getEntry(), pair.getMapping().bake()); + } + } + } + + @Nullable + private String formatLine(String line) { + line = stripComment(line); + line = line.trim(); + + if (line.isEmpty()) { + return null; + } + + return line; + } + + private String stripComment(String line) { + //Dont support comments on javadoc lines + if (line.trim().startsWith(EnigmaFormat.COMMENT)) { + return line; + } + + int commentPos = line.indexOf('#'); + if (commentPos >= 0) { + return line.substring(0, commentPos); + } + return line; + } + + private int countIndentation(String line) { + int indent = 0; + for (int i = 0; i < line.length(); i++) { + if (line.charAt(i) != '\t') { + break; + } + indent++; + } + return indent; + } + + private MappingPair parseLine(@Nullable MappingPair parent, String line) { + String[] tokens = line.trim().split("\\s"); + String keyToken = tokens[0].toUpperCase(Locale.ROOT); + Entry parentEntry = parent == null ? null : parent.getEntry(); + + switch (keyToken) { + case EnigmaFormat.CLASS: + return parseClass(parentEntry, tokens); + case EnigmaFormat.FIELD: + return parseField(parentEntry, tokens); + case EnigmaFormat.METHOD: + return parseMethod(parentEntry, tokens); + case EnigmaFormat.PARAMETER: + return parseArgument(parentEntry, tokens); + case EnigmaFormat.COMMENT: + readJavadoc(parent, tokens); + return null; + default: + throw new RuntimeException("Unknown token '" + keyToken + "'"); + } + } + + private void readJavadoc(MappingPair parent, String[] tokens) { + if (parent == null) + throw new IllegalStateException("Javadoc has no parent!"); + // Empty string to concat + String jdLine = tokens.length > 1 ? String.join(" ", Arrays.copyOfRange(tokens,1,tokens.length)) : ""; + if (parent.getMapping() == null) { + parent.setMapping(new RawEntryMapping(parent.getEntry().getName(), AccessModifier.UNCHANGED)); + } + parent.getMapping().addJavadocLine(MappingHelper.unescape(jdLine)); + } + + private MappingPair parseClass(@Nullable Entry parent, String[] tokens) { + String obfuscatedName = ClassEntry.getInnerName(tokens[1]); + ClassEntry obfuscatedEntry; + if (parent instanceof ClassEntry) { + obfuscatedEntry = new ClassEntry((ClassEntry) parent, obfuscatedName); + } else { + obfuscatedEntry = new ClassEntry(obfuscatedName); + } + + String mapping = null; + AccessModifier modifier = AccessModifier.UNCHANGED; + + if (tokens.length == 3) { + AccessModifier parsedModifier = parseModifier(tokens[2]); + if (parsedModifier != null) { + modifier = parsedModifier; + mapping = obfuscatedName; + } else { + mapping = tokens[2]; + } + } else if (tokens.length == 4) { + mapping = tokens[2]; + modifier = parseModifier(tokens[3]); + } + + if (mapping != null) { + return new MappingPair<>(obfuscatedEntry, new RawEntryMapping(mapping, modifier)); + } else { + return new MappingPair<>(obfuscatedEntry); + } + } + + private MappingPair parseField(@Nullable Entry parent, String[] tokens) { + if (!(parent instanceof ClassEntry)) { + throw new RuntimeException("Field must be a child of a class!"); + } + + ClassEntry ownerEntry = (ClassEntry) parent; + + String obfuscatedName = tokens[1]; + String mapping = obfuscatedName; + AccessModifier modifier = AccessModifier.UNCHANGED; + TypeDescriptor descriptor; + + if (tokens.length == 3) { + mapping = tokens[1]; + descriptor = new TypeDescriptor(tokens[2]); + } else if (tokens.length == 4) { + AccessModifier parsedModifier = parseModifier(tokens[3]); + if (parsedModifier != null) { + descriptor = new TypeDescriptor(tokens[2]); + modifier = parsedModifier; + } else { + mapping = tokens[2]; + descriptor = new TypeDescriptor(tokens[3]); + } + } else if (tokens.length == 5) { + descriptor = new TypeDescriptor(tokens[3]); + mapping = tokens[2]; + modifier = parseModifier(tokens[4]); + } else { + throw new RuntimeException("Invalid field declaration"); + } + + FieldEntry obfuscatedEntry = new FieldEntry(ownerEntry, obfuscatedName, descriptor); + if (mapping != null) { + return new MappingPair<>(obfuscatedEntry, new RawEntryMapping(mapping, modifier)); + } else { + return new MappingPair<>(obfuscatedEntry); + } + } + + private MappingPair parseMethod(@Nullable Entry parent, String[] tokens) { + if (!(parent instanceof ClassEntry)) { + throw new RuntimeException("Method must be a child of a class!"); + } + + ClassEntry ownerEntry = (ClassEntry) parent; + + String obfuscatedName = tokens[1]; + String mapping = null; + AccessModifier modifier = AccessModifier.UNCHANGED; + MethodDescriptor descriptor; + + if (tokens.length == 3) { + descriptor = new MethodDescriptor(tokens[2]); + } else if (tokens.length == 4) { + AccessModifier parsedModifier = parseModifier(tokens[3]); + if (parsedModifier != null) { + modifier = parsedModifier; + mapping = obfuscatedName; + descriptor = new MethodDescriptor(tokens[2]); + } else { + mapping = tokens[2]; + descriptor = new MethodDescriptor(tokens[3]); + } + } else if (tokens.length == 5) { + mapping = tokens[2]; + modifier = parseModifier(tokens[4]); + descriptor = new MethodDescriptor(tokens[3]); + } else { + throw new RuntimeException("Invalid method declaration"); + } + + MethodEntry obfuscatedEntry = new MethodEntry(ownerEntry, obfuscatedName, descriptor); + if (mapping != null) { + return new MappingPair<>(obfuscatedEntry, new RawEntryMapping(mapping, modifier)); + } else { + return new MappingPair<>(obfuscatedEntry); + } + } + + private MappingPair parseArgument(@Nullable Entry parent, String[] tokens) { + if (!(parent instanceof MethodEntry)) { + throw new RuntimeException("Method arg must be a child of a method!"); + } + + MethodEntry ownerEntry = (MethodEntry) parent; + LocalVariableEntry obfuscatedEntry = new LocalVariableEntry(ownerEntry, Integer.parseInt(tokens[1]), "", true, null); + String mapping = tokens[2]; + + return new MappingPair<>(obfuscatedEntry, new RawEntryMapping(mapping)); + } + + @Nullable + private AccessModifier parseModifier(String token) { + if (token.startsWith("ACC:")) { + return AccessModifier.valueOf(token.substring(4)); + } + return null; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/enigma/EnigmaMappingsWriter.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/enigma/EnigmaMappingsWriter.java new file mode 100644 index 0000000..7570d4b --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/enigma/EnigmaMappingsWriter.java @@ -0,0 +1,318 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.mapping.serde.enigma; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.MappingTranslator; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.AccessModifier; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingDelta; +import cuchaz.enigma.translation.mapping.serde.MappingFileNameFormat; +import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; +import cuchaz.enigma.translation.mapping.VoidEntryResolver; +import cuchaz.enigma.translation.mapping.serde.LfPrintWriter; +import cuchaz.enigma.translation.mapping.serde.MappingHelper; +import cuchaz.enigma.translation.mapping.serde.MappingsWriter; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import cuchaz.enigma.utils.I18n; + +public enum EnigmaMappingsWriter implements MappingsWriter { + FILE { + @Override + public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress, MappingSaveParameters saveParameters) { + Collection classes = mappings.getRootNodes() + .filter(entry -> entry instanceof ClassEntry) + .map(entry -> (ClassEntry) entry) + .collect(Collectors.toList()); + + progress.init(classes.size(), I18n.translate("progress.mappings.enigma_file.writing")); + + int steps = 0; + try (PrintWriter writer = new LfPrintWriter(Files.newBufferedWriter(path))) { + for (ClassEntry classEntry : classes) { + progress.step(steps++, classEntry.getFullName()); + writeRoot(writer, mappings, classEntry); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + }, + DIRECTORY { + @Override + public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress, MappingSaveParameters saveParameters) { + Collection changedClasses = delta.getChangedRoots() + .filter(entry -> entry instanceof ClassEntry) + .map(entry -> (ClassEntry) entry) + .collect(Collectors.toList()); + + applyDeletions(path, changedClasses, mappings, delta.getBaseMappings(), saveParameters.getFileNameFormat()); + + progress.init(changedClasses.size(), I18n.translate("progress.mappings.enigma_directory.writing")); + + AtomicInteger steps = new AtomicInteger(); + + Translator translator = new MappingTranslator(mappings, VoidEntryResolver.INSTANCE); + changedClasses.parallelStream().forEach(classEntry -> { + progress.step(steps.getAndIncrement(), classEntry.getFullName()); + + try { + ClassEntry fileEntry = classEntry; + if (saveParameters.getFileNameFormat() == MappingFileNameFormat.BY_DEOBF) { + fileEntry = translator.translate(fileEntry); + } + + Path classPath = resolve(path, fileEntry); + Files.createDirectories(classPath.getParent()); + Files.deleteIfExists(classPath); + + try (PrintWriter writer = new LfPrintWriter(Files.newBufferedWriter(classPath))) { + writeRoot(writer, mappings, classEntry); + } + } catch (Throwable t) { + System.err.println("Failed to write class '" + classEntry.getFullName() + "'"); + t.printStackTrace(); + } + }); + } + + private void applyDeletions(Path root, Collection changedClasses, EntryTree mappings, EntryTree oldMappings, MappingFileNameFormat fileNameFormat) { + Translator oldMappingTranslator = new MappingTranslator(oldMappings, VoidEntryResolver.INSTANCE); + + Stream deletedClassStream = changedClasses.stream() + .filter(e -> !Objects.equals(oldMappings.get(e), mappings.get(e))); + + if (fileNameFormat == MappingFileNameFormat.BY_DEOBF) { + deletedClassStream = deletedClassStream.map(oldMappingTranslator::translate); + } + + Collection deletedClasses = deletedClassStream.collect(Collectors.toList()); + + for (ClassEntry classEntry : deletedClasses) { + try { + Files.deleteIfExists(resolve(root, classEntry)); + } catch (IOException e) { + System.err.println("Failed to delete deleted class '" + classEntry + "'"); + e.printStackTrace(); + } + } + + for (ClassEntry classEntry : deletedClasses) { + String packageName = classEntry.getPackageName(); + if (packageName != null) { + Path packagePath = Paths.get(packageName); + try { + deleteDeadPackages(root, packagePath); + } catch (IOException e) { + System.err.println("Failed to delete dead package '" + packageName + "'"); + e.printStackTrace(); + } + } + } + } + + private void deleteDeadPackages(Path root, Path packagePath) throws IOException { + for (int i = packagePath.getNameCount() - 1; i >= 0; i--) { + Path subPath = packagePath.subpath(0, i + 1); + Path packagePart = root.resolve(subPath); + if (isEmpty(packagePart)) { + Files.deleteIfExists(packagePart); + } + } + } + + private boolean isEmpty(Path path) { + try (DirectoryStream stream = Files.newDirectoryStream(path)) { + return !stream.iterator().hasNext(); + } catch (IOException e) { + return false; + } + } + + private Path resolve(Path root, ClassEntry classEntry) { + return root.resolve(classEntry.getFullName() + ".mapping"); + } + }, + ZIP { + @Override + public void write(EntryTree mappings, MappingDelta delta, Path zip, ProgressListener progress, MappingSaveParameters saveParameters) { + try (FileSystem fs = FileSystems.newFileSystem(new URI("jar:file", null, zip.toUri().getPath(), ""), Collections.singletonMap("create", "true"))) { + DIRECTORY.write(mappings, delta, fs.getPath("/"), progress, saveParameters); + } catch (IOException e) { + e.printStackTrace(); + } catch (URISyntaxException e) { + throw new RuntimeException("Unexpected error creating URI for " + zip, e); + } + } + }; + + protected void writeRoot(PrintWriter writer, EntryTree mappings, ClassEntry classEntry) { + Collection> children = groupChildren(mappings.getChildren(classEntry)); + + EntryMapping classEntryMapping = mappings.get(classEntry); + + writer.println(writeClass(classEntry, classEntryMapping).trim()); + if (classEntryMapping != null && classEntryMapping.getJavadoc() != null) { + writeDocs(writer, classEntryMapping, 0); + } + + for (Entry child : children) { + writeEntry(writer, mappings, child, 1); + } + + } + + private void writeDocs(PrintWriter writer, EntryMapping mapping, int depth) { + String jd = mapping.getJavadoc(); + if (jd != null) { + for (String line : jd.split("\\R")) { + writer.println(indent(EnigmaFormat.COMMENT + " " + MappingHelper.escape(line), depth + 1)); + } + } + } + + protected void writeEntry(PrintWriter writer, EntryTree mappings, Entry entry, int depth) { + EntryTreeNode node = mappings.findNode(entry); + if (node == null) { + return; + } + + EntryMapping mapping = node.getValue(); + + if (entry instanceof ClassEntry) { + String line = writeClass((ClassEntry) entry, mapping); + writer.println(indent(line, depth)); + } else if (entry instanceof MethodEntry) { + String line = writeMethod((MethodEntry) entry, mapping); + writer.println(indent(line, depth)); + } else if (entry instanceof FieldEntry) { + String line = writeField((FieldEntry) entry, mapping); + writer.println(indent(line, depth)); + } else if (entry instanceof LocalVariableEntry && mapping != null) { + String line = writeArgument((LocalVariableEntry) entry, mapping); + writer.println(indent(line, depth)); + } + if (mapping != null && mapping.getJavadoc() != null) { + writeDocs(writer, mapping, depth); + } + + Collection> children = groupChildren(node.getChildren()); + for (Entry child : children) { + writeEntry(writer, mappings, child, depth + 1); + } + } + + private Collection> groupChildren(Collection> children) { + Collection> result = new ArrayList<>(children.size()); + + children.stream().filter(e -> e instanceof FieldEntry) + .map(e -> (FieldEntry) e) + .sorted() + .forEach(result::add); + + children.stream().filter(e -> e instanceof MethodEntry) + .map(e -> (MethodEntry) e) + .sorted() + .forEach(result::add); + + children.stream().filter(e -> e instanceof LocalVariableEntry) + .map(e -> (LocalVariableEntry) e) + .sorted() + .forEach(result::add); + + children.stream().filter(e -> e instanceof ClassEntry) + .map(e -> (ClassEntry) e) + .sorted() + .forEach(result::add); + + return result; + } + + protected String writeClass(ClassEntry entry, EntryMapping mapping) { + StringBuilder builder = new StringBuilder(EnigmaFormat.CLASS +" "); + builder.append(entry.getName()).append(' '); + writeMapping(builder, mapping); + + return builder.toString(); + } + + protected String writeMethod(MethodEntry entry, EntryMapping mapping) { + StringBuilder builder = new StringBuilder(EnigmaFormat.METHOD + " "); + builder.append(entry.getName()).append(' '); + if (mapping != null && !mapping.getTargetName().equals(entry.getName())) { + writeMapping(builder, mapping); + } + + builder.append(entry.getDesc().toString()); + + return builder.toString(); + } + + protected String writeField(FieldEntry entry, EntryMapping mapping) { + StringBuilder builder = new StringBuilder(EnigmaFormat.FIELD + " "); + builder.append(entry.getName()).append(' '); + if (mapping != null && !mapping.getTargetName().equals(entry.getName())) { + writeMapping(builder, mapping); + } + + builder.append(entry.getDesc().toString()); + + return builder.toString(); + } + + protected String writeArgument(LocalVariableEntry entry, EntryMapping mapping) { + return EnigmaFormat.PARAMETER + " " + entry.getIndex() + ' ' + mapping.getTargetName(); + } + + private void writeMapping(StringBuilder builder, EntryMapping mapping) { + if (mapping != null) { + builder.append(mapping.getTargetName()).append(' '); + if (mapping.getAccessModifier() != AccessModifier.UNCHANGED) { + builder.append(mapping.getAccessModifier().getFormattedName()).append(' '); + } + } + } + + private String indent(String line, int depth) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < depth; i++) { + builder.append("\t"); + } + builder.append(line.trim()); + return builder.toString(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/proguard/ProguardMappingsReader.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/proguard/ProguardMappingsReader.java new file mode 100644 index 0000000..034fb41 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/proguard/ProguardMappingsReader.java @@ -0,0 +1,135 @@ +package cuchaz.enigma.translation.mapping.serde.proguard; + +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.mapping.MappingOperations; +import cuchaz.enigma.translation.mapping.serde.MappingParseException; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; +import cuchaz.enigma.translation.mapping.serde.MappingsReader; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ProguardMappingsReader implements MappingsReader { + public static final ProguardMappingsReader INSTANCE = new ProguardMappingsReader(); + private static final String NAME = "[a-zA-Z0-9_\\-.$<>]+"; + private static final String TYPE = NAME + "(?:\\[])*"; + private static final String TYPE_LIST = "|(?:(?:" + TYPE + ",)*" + TYPE + ")"; + private static final Pattern CLASS = Pattern.compile("(" + NAME + ") -> (" + NAME + "):"); + private static final Pattern FIELD = Pattern.compile(" {4}(" + TYPE + ") (" + NAME + ") -> (" + NAME + ")"); + private static final Pattern METHOD = Pattern.compile(" {4}(?:[0-9]+:[0-9]+:)?(" + TYPE + ") (" + NAME + ")\\((" + TYPE_LIST + ")\\) -> (" + NAME + ")"); + + public ProguardMappingsReader() {} + + @Override + public EntryTree read(Path path, ProgressListener progress, MappingSaveParameters saveParameters) throws MappingParseException, IOException { + EntryTree mappings = new HashEntryTree<>(); + + int lineNumber = 0; + ClassEntry currentClass = null; + for (String line : Files.readAllLines(path, StandardCharsets.UTF_8)) { + lineNumber++; + + if (line.startsWith("#") || line.isEmpty()) { + continue; + } + + Matcher classMatcher = CLASS.matcher(line); + Matcher fieldMatcher = FIELD.matcher(line); + Matcher methodMatcher = METHOD.matcher(line); + + if (classMatcher.matches()) { + String name = classMatcher.group(1); + String targetName = classMatcher.group(2); + + mappings.insert(currentClass = new ClassEntry(name.replace('.', '/')), new EntryMapping(ClassEntry.getInnerName(targetName.replace('.', '/')))); + } else if (fieldMatcher.matches()) { + String type = fieldMatcher.group(1); + String name = fieldMatcher.group(2); + String targetName = fieldMatcher.group(3); + + if (currentClass == null) { + throw new MappingParseException(path::toString, lineNumber, "field mapping not inside class: " + line); + } + + mappings.insert(new FieldEntry(currentClass, name, new TypeDescriptor(getDescriptor(type))), new EntryMapping(targetName)); + } else if (methodMatcher.matches()) { + String returnType = methodMatcher.group(1); + String name = methodMatcher.group(2); + String[] parameterTypes = methodMatcher.group(3).isEmpty() ? new String[0] : methodMatcher.group(3).split(","); + String targetName = methodMatcher.group(4); + + if (currentClass == null) { + throw new MappingParseException(path::toString, lineNumber, "method mapping not inside class: " + line); + } + + mappings.insert(new MethodEntry(currentClass, name, new MethodDescriptor(getDescriptor(returnType, parameterTypes))), new EntryMapping(targetName)); + } else { + throw new MappingParseException(path::toString, lineNumber, "invalid mapping line: " + line); + } + } + + return MappingOperations.invert(mappings); + } + + private String getDescriptor(String type) { + StringBuilder descriptor = new StringBuilder(); + + while (type.endsWith("[]")) { + descriptor.append("["); + type = type.substring(0, type.length() - 2); + } + + switch (type) { + case "byte": + return descriptor + "B"; + case "char": + return descriptor + "C"; + case "short": + return descriptor + "S"; + case "int": + return descriptor + "I"; + case "long": + return descriptor + "J"; + case "float": + return descriptor + "F"; + case "double": + return descriptor + "D"; + case "boolean": + return descriptor + "Z"; + case "void": + return descriptor + "V"; + } + + descriptor.append("L"); + descriptor.append(type.replace('.', '/')); + descriptor.append(";"); + + return descriptor.toString(); + } + + private String getDescriptor(String returnType, String[] parameterTypes) { + StringBuilder descriptor = new StringBuilder(); + descriptor.append('('); + + for (String parameterType : parameterTypes) { + descriptor.append(getDescriptor(parameterType)); + } + + descriptor.append(')'); + descriptor.append(getDescriptor(returnType)); + + return descriptor.toString(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/srg/SrgMappingsWriter.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/srg/SrgMappingsWriter.java new file mode 100644 index 0000000..9c9f103 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/srg/SrgMappingsWriter.java @@ -0,0 +1,119 @@ +package cuchaz.enigma.translation.mapping.serde.srg; + +import com.google.common.collect.Lists; +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.MappingTranslator; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingDelta; +import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; +import cuchaz.enigma.translation.mapping.VoidEntryResolver; +import cuchaz.enigma.translation.mapping.serde.LfPrintWriter; +import cuchaz.enigma.translation.mapping.serde.MappingsWriter; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import cuchaz.enigma.utils.I18n; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public enum SrgMappingsWriter implements MappingsWriter { + INSTANCE; + + @Override + public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress, MappingSaveParameters saveParameters) { + try { + Files.deleteIfExists(path); + Files.createFile(path); + } catch (IOException e) { + e.printStackTrace(); + } + + List classLines = new ArrayList<>(); + List fieldLines = new ArrayList<>(); + List methodLines = new ArrayList<>(); + + Collection> rootEntries = Lists.newArrayList(mappings).stream() + .map(EntryTreeNode::getEntry) + .collect(Collectors.toList()); + progress.init(rootEntries.size(), I18n.translate("progress.mappings.srg_file.generating")); + + int steps = 0; + for (Entry entry : sorted(rootEntries)) { + progress.step(steps++, entry.getName()); + writeEntry(classLines, fieldLines, methodLines, mappings, entry); + } + + progress.init(3, I18n.translate("progress.mappings.srg_file.writing")); + try (PrintWriter writer = new LfPrintWriter(Files.newBufferedWriter(path))) { + progress.step(0, I18n.translate("type.classes")); + classLines.forEach(writer::println); + progress.step(1, I18n.translate("type.fields")); + fieldLines.forEach(writer::println); + progress.step(2, I18n.translate("type.methods")); + methodLines.forEach(writer::println); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void writeEntry(List classes, List fields, List methods, EntryTree mappings, Entry entry) { + EntryTreeNode node = mappings.findNode(entry); + if (node == null) { + return; + } + + Translator translator = new MappingTranslator(mappings, VoidEntryResolver.INSTANCE); + if (entry instanceof ClassEntry) { + classes.add(generateClassLine((ClassEntry) entry, translator)); + } else if (entry instanceof FieldEntry) { + fields.add(generateFieldLine((FieldEntry) entry, translator)); + } else if (entry instanceof MethodEntry) { + methods.add(generateMethodLine((MethodEntry) entry, translator)); + } + + for (Entry child : sorted(node.getChildren())) { + writeEntry(classes, fields, methods, mappings, child); + } + } + + private String generateClassLine(ClassEntry sourceEntry, Translator translator) { + ClassEntry targetEntry = translator.translate(sourceEntry); + return "CL: " + sourceEntry.getFullName() + " " + targetEntry.getFullName(); + } + + private String generateMethodLine(MethodEntry sourceEntry, Translator translator) { + MethodEntry targetEntry = translator.translate(sourceEntry); + return "MD: " + describeMethod(sourceEntry) + " " + describeMethod(targetEntry); + } + + private String describeMethod(MethodEntry entry) { + return entry.getParent().getFullName() + "/" + entry.getName() + " " + entry.getDesc(); + } + + private String generateFieldLine(FieldEntry sourceEntry, Translator translator) { + FieldEntry targetEntry = translator.translate(sourceEntry); + return "FD: " + describeField(sourceEntry) + " " + describeField(targetEntry); + } + + private String describeField(FieldEntry entry) { + return entry.getParent().getFullName() + "/" + entry.getName(); + } + + private Collection> sorted(Iterable> iterable) { + ArrayList> sorted = Lists.newArrayList(iterable); + sorted.sort(Comparator.comparing(Entry::getName)); + return sorted; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tiny/TinyMappingsReader.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tiny/TinyMappingsReader.java new file mode 100644 index 0000000..f3c66df --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tiny/TinyMappingsReader.java @@ -0,0 +1,116 @@ +package cuchaz.enigma.translation.mapping.serde.tiny; + +import com.google.common.base.Charsets; +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.mapping.serde.MappingParseException; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingPair; +import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; +import cuchaz.enigma.translation.mapping.serde.MappingsReader; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import cuchaz.enigma.utils.I18n; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public enum TinyMappingsReader implements MappingsReader { + INSTANCE; + + @Override + public EntryTree read(Path path, ProgressListener progress, MappingSaveParameters saveParameters) throws IOException, MappingParseException { + return read(path, Files.readAllLines(path, Charsets.UTF_8), progress); + } + + private EntryTree read(Path path, List lines, ProgressListener progress) throws MappingParseException { + EntryTree mappings = new HashEntryTree<>(); + lines.remove(0); + + progress.init(lines.size(), I18n.translate("progress.mappings.tiny_file.loading")); + + for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { + progress.step(lineNumber, ""); + + String line = lines.get(lineNumber); + + if (line.trim().startsWith("#")) { + continue; + } + + try { + MappingPair mapping = parseLine(line); + mappings.insert(mapping.getEntry(), mapping.getMapping()); + } catch (Throwable t) { + t.printStackTrace(); + throw new MappingParseException(path::toString, lineNumber, t.toString()); + } + } + + return mappings; + } + + private MappingPair parseLine(String line) { + String[] tokens = line.split("\t"); + + String key = tokens[0]; + switch (key) { + case "CLASS": + return parseClass(tokens); + case "FIELD": + return parseField(tokens); + case "METHOD": + return parseMethod(tokens); + case "MTH-ARG": + return parseArgument(tokens); + default: + throw new RuntimeException("Unknown token '" + key + "'!"); + } + } + + private MappingPair parseClass(String[] tokens) { + ClassEntry obfuscatedEntry = new ClassEntry(tokens[1]); + String mapping = tokens[2]; + if (mapping.indexOf('$') > 0) { + // inner classes should map to only the final part + mapping = mapping.substring(mapping.lastIndexOf('$') + 1); + } + return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); + } + + private MappingPair parseField(String[] tokens) { + ClassEntry ownerClass = new ClassEntry(tokens[1]); + TypeDescriptor descriptor = new TypeDescriptor(tokens[2]); + + FieldEntry obfuscatedEntry = new FieldEntry(ownerClass, tokens[3], descriptor); + String mapping = tokens[4]; + return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); + } + + private MappingPair parseMethod(String[] tokens) { + ClassEntry ownerClass = new ClassEntry(tokens[1]); + MethodDescriptor descriptor = new MethodDescriptor(tokens[2]); + + MethodEntry obfuscatedEntry = new MethodEntry(ownerClass, tokens[3], descriptor); + String mapping = tokens[4]; + return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); + } + + private MappingPair parseArgument(String[] tokens) { + ClassEntry ownerClass = new ClassEntry(tokens[1]); + MethodDescriptor ownerDescriptor = new MethodDescriptor(tokens[2]); + MethodEntry ownerMethod = new MethodEntry(ownerClass, tokens[3], ownerDescriptor); + int variableIndex = Integer.parseInt(tokens[4]); + + String mapping = tokens[5]; + LocalVariableEntry obfuscatedEntry = new LocalVariableEntry(ownerMethod, variableIndex, "", true, null); + return new MappingPair<>(obfuscatedEntry, new EntryMapping(mapping)); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tiny/TinyMappingsWriter.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tiny/TinyMappingsWriter.java new file mode 100644 index 0000000..4f78e6f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tiny/TinyMappingsWriter.java @@ -0,0 +1,149 @@ +package cuchaz.enigma.translation.mapping.serde.tiny; + +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.MappingTranslator; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingDelta; +import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; +import cuchaz.enigma.translation.mapping.VoidEntryResolver; +import cuchaz.enigma.translation.mapping.serde.MappingsWriter; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; + +public class TinyMappingsWriter implements MappingsWriter { + private static final String VERSION_CONSTANT = "v1"; + private static final Joiner TAB_JOINER = Joiner.on('\t'); + + //Possibly add a gui or a way to select the namespaces when exporting from the gui + public static final TinyMappingsWriter INSTANCE = new TinyMappingsWriter("intermediary", "named"); + + // HACK: as of enigma 0.13.1, some fields seem to appear duplicated? + private final Set writtenLines = new HashSet<>(); + private final String nameObf; + private final String nameDeobf; + + public TinyMappingsWriter(String nameObf, String nameDeobf) { + this.nameObf = nameObf; + this.nameDeobf = nameDeobf; + } + + @Override + public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress, MappingSaveParameters saveParameters) { + try { + Files.deleteIfExists(path); + Files.createFile(path); + } catch (IOException e) { + e.printStackTrace(); + } + + try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { + writeLine(writer, new String[]{VERSION_CONSTANT, nameObf, nameDeobf}); + + Lists.newArrayList(mappings).stream() + .map(EntryTreeNode::getEntry).sorted(Comparator.comparing(Object::toString)) + .forEach(entry -> writeEntry(writer, mappings, entry)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void writeEntry(Writer writer, EntryTree mappings, Entry entry) { + EntryTreeNode node = mappings.findNode(entry); + if (node == null) { + return; + } + + Translator translator = new MappingTranslator(mappings, VoidEntryResolver.INSTANCE); + + EntryMapping mapping = mappings.get(entry); + if (mapping != null && !entry.getName().equals(mapping.getTargetName())) { + if (entry instanceof ClassEntry) { + writeClass(writer, (ClassEntry) entry, translator); + } else if (entry instanceof FieldEntry) { + writeLine(writer, serializeEntry(entry, mapping.getTargetName())); + } else if (entry instanceof MethodEntry) { + writeLine(writer, serializeEntry(entry, mapping.getTargetName())); + } + } + + writeChildren(writer, mappings, node); + } + + private void writeChildren(Writer writer, EntryTree mappings, EntryTreeNode node) { + node.getChildren().stream() + .filter(e -> e instanceof FieldEntry).sorted() + .forEach(child -> writeEntry(writer, mappings, child)); + + node.getChildren().stream() + .filter(e -> e instanceof MethodEntry).sorted() + .forEach(child -> writeEntry(writer, mappings, child)); + + node.getChildren().stream() + .filter(e -> e instanceof ClassEntry).sorted() + .forEach(child -> writeEntry(writer, mappings, child)); + } + + private void writeClass(Writer writer, ClassEntry entry, Translator translator) { + ClassEntry translatedEntry = translator.translate(entry); + + String obfClassName = entry.getFullName(); + String deobfClassName = translatedEntry.getFullName(); + writeLine(writer, new String[]{"CLASS", obfClassName, deobfClassName}); + } + + private void writeLine(Writer writer, String[] data) { + try { + String line = TAB_JOINER.join(data) + "\n"; + if (writtenLines.add(line)) { + writer.write(line); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String[] serializeEntry(Entry entry, String... extraFields) { + String[] data = null; + + if (entry instanceof FieldEntry) { + data = new String[4 + extraFields.length]; + data[0] = "FIELD"; + data[1] = entry.getContainingClass().getFullName(); + data[2] = ((FieldEntry) entry).getDesc().toString(); + data[3] = entry.getName(); + } else if (entry instanceof MethodEntry) { + data = new String[4 + extraFields.length]; + data[0] = "METHOD"; + data[1] = entry.getContainingClass().getFullName(); + data[2] = ((MethodEntry) entry).getDesc().toString(); + data[3] = entry.getName(); + } else if (entry instanceof ClassEntry) { + data = new String[2 + extraFields.length]; + data[0] = "CLASS"; + data[1] = ((ClassEntry) entry).getFullName(); + } + + if (data != null) { + System.arraycopy(extraFields, 0, data, data.length - extraFields.length, extraFields.length); + } + + return data; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tinyv2/TinyV2Reader.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tinyv2/TinyV2Reader.java new file mode 100644 index 0000000..dc25569 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tinyv2/TinyV2Reader.java @@ -0,0 +1,296 @@ +package cuchaz.enigma.translation.mapping.serde.tinyv2; + +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.mapping.serde.MappingParseException; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingPair; +import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; +import cuchaz.enigma.translation.mapping.serde.MappingsReader; +import cuchaz.enigma.translation.mapping.serde.RawEntryMapping; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.BitSet; +import java.util.List; + +public final class TinyV2Reader implements MappingsReader { + + private static final String MINOR_VERSION = "0"; + // 0 indent + private static final int IN_HEADER = 0; + private static final int IN_CLASS = IN_HEADER + 1; + // 1 indent + private static final int IN_METHOD = IN_CLASS + 1; + private static final int IN_FIELD = IN_METHOD + 1; + // 2 indent + private static final int IN_PARAMETER = IN_FIELD + 1; + // general properties + private static final int STATE_SIZE = IN_PARAMETER + 1; + private static final int[] INDENT_CLEAR_START = {IN_HEADER, IN_METHOD, IN_PARAMETER, STATE_SIZE}; + + @Override + public EntryTree read(Path path, ProgressListener progress, MappingSaveParameters saveParameters) throws IOException, MappingParseException { + return read(path, Files.readAllLines(path, StandardCharsets.UTF_8), progress); + } + + private EntryTree read(Path path, List lines, ProgressListener progress) throws MappingParseException { + EntryTree mappings = new HashEntryTree<>(); + + progress.init(lines.size(), "progress.mappings.tiny_v2.loading"); + + BitSet state = new BitSet(STATE_SIZE); + @SuppressWarnings({"unchecked", "rawtypes"}) + MappingPair, RawEntryMapping>[] holds = new MappingPair[STATE_SIZE]; + boolean escapeNames = false; + + for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { + try { + progress.step(lineNumber, ""); + String line = lines.get(lineNumber); + + int indent = 0; + while (line.charAt(indent) == '\t') + indent++; + + String[] parts = line.substring(indent).split("\t", -1); + if (parts.length == 0 || indent >= INDENT_CLEAR_START.length) + throw new IllegalArgumentException("Invalid format"); + + // clean and register stuff in stack + for (int i = INDENT_CLEAR_START[indent]; i < STATE_SIZE; i++) { + state.clear(i); + if (holds[i] != null) { + RawEntryMapping mapping = holds[i].getMapping(); + if (mapping != null) { + EntryMapping baked = mapping.bake(); + if (baked != null) { + mappings.insert(holds[i].getEntry(), baked); + } + } + holds[i] = null; + } + } + + switch (indent) { + case 0: + switch (parts[0]) { + case "tiny": // header + if (lineNumber != 0) { + throw new IllegalArgumentException("Header can only be on the first line"); + } + if (parts.length < 5) { + throw new IllegalArgumentException("Not enough header columns, needs at least 5"); + } + if (!"2".equals(parts[1]) || !MINOR_VERSION.equals(parts[2])) { + throw new IllegalArgumentException("Unsupported TinyV2 version, requires major " + "2" + " and minor " + MINOR_VERSION + ""); + } + state.set(IN_HEADER); + break; + case "c": // class + state.set(IN_CLASS); + holds[IN_CLASS] = parseClass(parts, escapeNames); + break; + default: + unsupportKey(parts); + } + + break; + case 1: + if (state.get(IN_HEADER)) { + if (parts[0].equals("esacpe-names")) { + escapeNames = true; + } + + break; + } + + if (state.get(IN_CLASS)) { + switch (parts[0]) { + case "m": // method + state.set(IN_METHOD); + holds[IN_METHOD] = parseMethod(holds[IN_CLASS], parts, escapeNames); + break; + case "f": // field + state.set(IN_FIELD); + holds[IN_FIELD] = parseField(holds[IN_CLASS], parts, escapeNames); + break; + case "c": // class javadoc + addJavadoc(holds[IN_CLASS], parts); + break; + default: + unsupportKey(parts); + } + break; + } + + unsupportKey(parts); + case 2: + if (state.get(IN_METHOD)) { + switch (parts[0]) { + case "p": // parameter + state.set(IN_PARAMETER); + holds[IN_PARAMETER] = parseArgument(holds[IN_METHOD], parts, escapeNames); + break; + case "v": // local variable + // TODO add local var mapping + break; + case "c": // method javadoc + addJavadoc(holds[IN_METHOD], parts); + break; + default: + unsupportKey(parts); + } + break; + } + + if (state.get(IN_FIELD)) { + switch (parts[0]) { + case "c": // field javadoc + addJavadoc(holds[IN_FIELD], parts); + break; + default: + unsupportKey(parts); + } + break; + } + unsupportKey(parts); + case 3: + if (state.get(IN_PARAMETER)) { + switch (parts[0]) { + case "c": + addJavadoc(holds[IN_PARAMETER], parts); + break; + default: + unsupportKey(parts); + } + break; + } + unsupportKey(parts); + default: + unsupportKey(parts); + } + + } catch (Throwable t) { + t.printStackTrace(); + throw new MappingParseException(path::toString, lineNumber + 1, t.toString()); + } + } + + return mappings; + } + + private void unsupportKey(String[] parts) { + throw new IllegalArgumentException("Unsupported key " + parts[0]); + } + + private void addJavadoc(MappingPair pair, String[] parts) { + if (parts.length != 2) { + throw new IllegalArgumentException("Invalid javadoc declaration"); + } + + addJavadoc(pair, parts[1]); + } + + private MappingPair parseClass(String[] tokens, boolean escapeNames) { + ClassEntry obfuscatedEntry = new ClassEntry(unescapeOpt(tokens[1], escapeNames)); + if (tokens.length <= 2) + return new MappingPair<>(obfuscatedEntry); + String token2 = unescapeOpt(tokens[2], escapeNames); + String mapping = token2.substring(token2.lastIndexOf('$') + 1); + return new MappingPair<>(obfuscatedEntry, new RawEntryMapping(mapping)); + } + + private MappingPair parseField(MappingPair parent, String[] tokens, boolean escapeNames) { + ClassEntry ownerClass = (ClassEntry) parent.getEntry(); + TypeDescriptor descriptor = new TypeDescriptor(unescapeOpt(tokens[1], escapeNames)); + + FieldEntry obfuscatedEntry = new FieldEntry(ownerClass, unescapeOpt(tokens[2], escapeNames), descriptor); + if (tokens.length <= 3) + return new MappingPair<>(obfuscatedEntry); + String mapping = unescapeOpt(tokens[3], escapeNames); + return new MappingPair<>(obfuscatedEntry, new RawEntryMapping(mapping)); + } + + private MappingPair parseMethod(MappingPair parent, String[] tokens, boolean escapeNames) { + ClassEntry ownerClass = (ClassEntry) parent.getEntry(); + MethodDescriptor descriptor = new MethodDescriptor(unescapeOpt(tokens[1], escapeNames)); + + MethodEntry obfuscatedEntry = new MethodEntry(ownerClass, unescapeOpt(tokens[2], escapeNames), descriptor); + if (tokens.length <= 3) + return new MappingPair<>(obfuscatedEntry); + String mapping = unescapeOpt(tokens[3], escapeNames); + return new MappingPair<>(obfuscatedEntry, new RawEntryMapping(mapping)); + } + + + + private void addJavadoc(MappingPair pair, String javadoc) { + RawEntryMapping mapping = pair.getMapping(); + if (mapping == null) { + throw new IllegalArgumentException("Javadoc requires a mapping in enigma!"); + } + mapping.addJavadocLine(unescape(javadoc)); + } + + + + private MappingPair parseArgument(MappingPair parent, String[] tokens, boolean escapeNames) { + MethodEntry ownerMethod = (MethodEntry) parent.getEntry(); + int variableIndex = Integer.parseInt(tokens[1]); + + // tokens[2] is the useless obf name + + LocalVariableEntry obfuscatedEntry = new LocalVariableEntry(ownerMethod, variableIndex, "", true, null); + if (tokens.length <= 3) + return new MappingPair<>(obfuscatedEntry); + String mapping = unescapeOpt(tokens[3], escapeNames); + return new MappingPair<>(obfuscatedEntry, new RawEntryMapping(mapping)); + } + + private static final String TO_ESCAPE = "\\\n\r\0\t"; + private static final String ESCAPED = "\\nr0t"; + + private static String unescapeOpt(String raw, boolean escapedStrings) { + return escapedStrings ? unescape(raw) : raw; + } + + private static String unescape(String str) { + // copied from matcher, lazy! + int pos = str.indexOf('\\'); + if (pos < 0) return str; + + StringBuilder ret = new StringBuilder(str.length() - 1); + int start = 0; + + do { + ret.append(str, start, pos); + pos++; + int type; + + if (pos >= str.length()) { + throw new RuntimeException("incomplete escape sequence at the end"); + } else if ((type = ESCAPED.indexOf(str.charAt(pos))) < 0) { + throw new RuntimeException("invalid escape character: \\" + str.charAt(pos)); + } else { + ret.append(TO_ESCAPE.charAt(type)); + } + + start = pos + 1; + } while ((pos = str.indexOf('\\', start)) >= 0); + + ret.append(str, start, str.length()); + + return ret.toString(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tinyv2/TinyV2Writer.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tinyv2/TinyV2Writer.java new file mode 100644 index 0000000..5546b2f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/serde/tinyv2/TinyV2Writer.java @@ -0,0 +1,172 @@ +package cuchaz.enigma.translation.mapping.serde.tinyv2; + +import com.google.common.base.Strings; +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.MappingDelta; +import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; +import cuchaz.enigma.translation.mapping.serde.LfPrintWriter; +import cuchaz.enigma.translation.mapping.serde.MappingHelper; +import cuchaz.enigma.translation.mapping.serde.MappingsWriter; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public final class TinyV2Writer implements MappingsWriter { + + private static final String MINOR_VERSION = "0"; + private final String obfHeader; + private final String deobfHeader; + + public TinyV2Writer(String obfHeader, String deobfHeader) { + this.obfHeader = obfHeader; + this.deobfHeader = deobfHeader; + } + + @Override + public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress, MappingSaveParameters parameters) { + List> classes = StreamSupport.stream(mappings.spliterator(), false).filter(node -> node.getEntry() instanceof ClassEntry).collect(Collectors.toList()); + + try (PrintWriter writer = new LfPrintWriter(Files.newBufferedWriter(path))) { + writer.println("tiny\t2\t" + MINOR_VERSION + "\t" + obfHeader + "\t" + deobfHeader); + + // no escape names + + for (EntryTreeNode node : classes) { + writeClass(writer, node, mappings); + } + } catch (IOException ex) { + ex.printStackTrace(); // TODO add some better logging system + } + } + + private void writeClass(PrintWriter writer, EntryTreeNode node, EntryMap tree) { + writer.print("c\t"); + ClassEntry classEntry = (ClassEntry) node.getEntry(); + String fullName = classEntry.getFullName(); + writer.print(fullName); + Deque parts = new LinkedList<>(); + do { + EntryMapping mapping = tree.get(classEntry); + if (mapping != null) { + parts.addFirst(mapping.getTargetName()); + } else { + parts.addFirst(classEntry.getName()); + } + classEntry = classEntry.getOuterClass(); + } while (classEntry != null); + + String mappedName = String.join("$", parts); + + writer.print("\t"); + + writer.print(mappedName); // todo escaping when we have v2 fixed later + + writer.println(); + + writeComment(writer, node.getValue(), 1); + + for (EntryTreeNode child : node.getChildNodes()) { + Entry entry = child.getEntry(); + if (entry instanceof FieldEntry) { + writeField(writer, child); + } else if (entry instanceof MethodEntry) { + writeMethod(writer, child); + } + } + } + + private void writeMethod(PrintWriter writer, EntryTreeNode node) { + writer.print(indent(1)); + writer.print("m\t"); + writer.print(((MethodEntry) node.getEntry()).getDesc().toString()); + writer.print("\t"); + writer.print(node.getEntry().getName()); + writer.print("\t"); + EntryMapping mapping = node.getValue(); + if (mapping == null) { + writer.println(node.getEntry().getName()); // todo fix v2 name inference + } else { + writer.println(mapping.getTargetName()); + + writeComment(writer, mapping, 2); + } + + for (EntryTreeNode child : node.getChildNodes()) { + Entry entry = child.getEntry(); + if (entry instanceof LocalVariableEntry) { + writeParameter(writer, child); + } + // TODO write actual local variables + } + } + + private void writeField(PrintWriter writer, EntryTreeNode node) { + if (node.getValue() == null) + return; // Shortcut + + writer.print(indent(1)); + writer.print("f\t"); + writer.print(((FieldEntry) node.getEntry()).getDesc().toString()); + writer.print("\t"); + writer.print(node.getEntry().getName()); + writer.print("\t"); + EntryMapping mapping = node.getValue(); + if (mapping == null) { + writer.println(node.getEntry().getName()); // todo fix v2 name inference + } else { + writer.println(mapping.getTargetName()); + + writeComment(writer, mapping, 2); + } + } + + private void writeParameter(PrintWriter writer, EntryTreeNode node) { + if (node.getValue() == null) + return; // Shortcut + + writer.print(indent(2)); + writer.print("p\t"); + writer.print(((LocalVariableEntry) node.getEntry()).getIndex()); + writer.print("\t"); + writer.print(node.getEntry().getName()); + writer.print("\t"); + EntryMapping mapping = node.getValue(); + if (mapping == null) { + writer.println(); // todo ??? + } else { + writer.println(mapping.getTargetName()); + + writeComment(writer, mapping, 3); + } + } + + private void writeComment(PrintWriter writer, EntryMapping mapping, int indent) { + if (mapping != null && mapping.getJavadoc() != null) { + writer.print(indent(indent)); + writer.print("c\t"); + writer.print(MappingHelper.escape(mapping.getJavadoc())); + writer.println(); + } + } + + private String indent(int level) { + return Strings.repeat("\t", level); + } + +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/DeltaTrackingTree.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/DeltaTrackingTree.java new file mode 100644 index 0000000..255fa5f --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/DeltaTrackingTree.java @@ -0,0 +1,110 @@ +package cuchaz.enigma.translation.mapping.tree; + +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.MappingDelta; +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Iterator; +import java.util.stream.Stream; + +public class DeltaTrackingTree implements EntryTree { + private final EntryTree delegate; + + private EntryTree deltaReference; + private EntryTree changes = new HashEntryTree<>(); + + public DeltaTrackingTree(EntryTree delegate) { + this.delegate = delegate; + this.deltaReference = new HashEntryTree<>(delegate); + } + + public DeltaTrackingTree() { + this(new HashEntryTree<>()); + } + + @Override + public void insert(Entry entry, T value) { + trackChange(entry); + delegate.insert(entry, value); + } + + @Nullable + @Override + public T remove(Entry entry) { + trackChange(entry); + return delegate.remove(entry); + } + + public void trackChange(Entry entry) { + changes.insert(entry, MappingDelta.PLACEHOLDER); + } + + @Nullable + @Override + public T get(Entry entry) { + return delegate.get(entry); + } + + @Override + public Collection> getChildren(Entry entry) { + return delegate.getChildren(entry); + } + + @Override + public Collection> getSiblings(Entry entry) { + return delegate.getSiblings(entry); + } + + @Nullable + @Override + public EntryTreeNode findNode(Entry entry) { + return delegate.findNode(entry); + } + + @Override + public Stream> getRootNodes() { + return delegate.getRootNodes(); + } + + @Override + public DeltaTrackingTree translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + DeltaTrackingTree translatedTree = new DeltaTrackingTree<>(delegate.translate(translator, resolver, mappings)); + translatedTree.changes = changes.translate(translator, resolver, mappings); + return translatedTree; + } + + @Override + public Stream> getAllEntries() { + return delegate.getAllEntries(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public Iterator> iterator() { + return delegate.iterator(); + } + + public MappingDelta takeDelta() { + MappingDelta delta = new MappingDelta<>(deltaReference, changes); + resetDelta(); + return delta; + } + + private void resetDelta() { + deltaReference = new HashEntryTree<>(delegate); + changes = new HashEntryTree<>(); + } + + public boolean isDirty() { + return !changes.isEmpty(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTree.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTree.java new file mode 100644 index 0000000..daaefcc --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTree.java @@ -0,0 +1,26 @@ +package cuchaz.enigma.translation.mapping.tree; + +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.stream.Stream; + +public interface EntryTree extends EntryMap, Iterable>, Translatable { + Collection> getChildren(Entry entry); + + Collection> getSiblings(Entry entry); + + @Nullable + EntryTreeNode findNode(Entry entry); + + Stream> getRootNodes(); + + @Override + EntryTree translate(Translator translator, EntryResolver resolver, EntryMap mappings); +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTreeNode.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTreeNode.java new file mode 100644 index 0000000..affcd50 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/EntryTreeNode.java @@ -0,0 +1,40 @@ +package cuchaz.enigma.translation.mapping.tree; + +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Collectors; + +public interface EntryTreeNode { + @Nullable + T getValue(); + + Entry getEntry(); + + boolean isEmpty(); + + Collection> getChildren(); + + Collection> getChildNodes(); + + default Collection> getNodesRecursively() { + Collection> nodes = new ArrayList<>(); + nodes.add(this); + for (EntryTreeNode node : getChildNodes()) { + nodes.addAll(node.getNodesRecursively()); + } + return nodes; + } + + default Collection> getChildrenRecursively() { + return getNodesRecursively().stream() + .map(EntryTreeNode::getEntry) + .collect(Collectors.toList()); + } + + default boolean hasValue() { + return getValue() != null; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/HashEntryTree.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/HashEntryTree.java new file mode 100644 index 0000000..570941c --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/HashEntryTree.java @@ -0,0 +1,188 @@ +package cuchaz.enigma.translation.mapping.tree; + +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public class HashEntryTree implements EntryTree { + private final Map, HashTreeNode> root = new HashMap<>(); + + public HashEntryTree() { + } + + public HashEntryTree(EntryTree tree) { + for (EntryTreeNode node : tree) { + insert(node.getEntry(), node.getValue()); + } + } + + @Override + public void insert(Entry entry, T value) { + List> path = computePath(entry, true); + path.get(path.size() - 1).putValue(value); + if (value == null) { + removeDeadAlong(path); + } + } + + @Override + @Nullable + public T remove(Entry entry) { + List> path = computePath(entry, false); + if (path.isEmpty()) { + return null; + } + + T value = path.get(path.size() - 1).removeValue(); + + removeDeadAlong(path); + + return value; + } + + @Override + @Nullable + public T get(Entry entry) { + HashTreeNode node = findNode(entry); + if (node == null) { + return null; + } + return node.getValue(); + } + + @Override + public boolean contains(Entry entry) { + return get(entry) != null; + } + + @Override + public Collection> getChildren(Entry entry) { + HashTreeNode leaf = findNode(entry); + if (leaf == null) { + return Collections.emptyList(); + } + return leaf.getChildren(); + } + + @Override + public Collection> getSiblings(Entry entry) { + Entry parent = entry.getParent(); + if (parent == null) { + return getSiblings(entry, root.keySet()); + } + return getSiblings(entry, getChildren(parent)); + } + + private Collection> getSiblings(Entry entry, Collection> generation) { + Set> siblings = new HashSet<>(generation); + siblings.remove(entry); + return siblings; + } + + @Override + @Nullable + public HashTreeNode findNode(Entry target) { + List> parentChain = target.getAncestry(); + if (parentChain.isEmpty()) { + return null; + } + + HashTreeNode node = root.get(parentChain.get(0)); + for (int i = 1; i < parentChain.size(); i++) { + if (node == null) { + return null; + } + node = node.getChild(parentChain.get(i)); + } + + return node; + } + + private List> computePath(Entry target, boolean make) { + List> ancestry = target.getAncestry(); + if (ancestry.isEmpty()) { + return Collections.emptyList(); + } + + List> path = new ArrayList<>(ancestry.size()); + + Entry rootEntry = ancestry.get(0); + HashTreeNode node = make ? root.computeIfAbsent(rootEntry, HashTreeNode::new) : root.get(rootEntry); + if (node == null) { + return Collections.emptyList(); + } + + path.add(node); + + for (int i = 1; i < ancestry.size(); i++) { + Entry ancestor = ancestry.get(i); + node = make ? node.computeChild(ancestor) : node.getChild(ancestor); + if (node == null) { + return Collections.emptyList(); + } + + path.add(node); + } + + return path; + } + + private void removeDeadAlong(List> path) { + for (int i = path.size() - 1; i >= 0; i--) { + HashTreeNode node = path.get(i); + if (node.isEmpty()) { + if (i > 0) { + HashTreeNode parentNode = path.get(i - 1); + parentNode.remove(node.getEntry()); + } else { + root.remove(node.getEntry()); + } + } else { + break; + } + } + } + + @Override + public Iterator> iterator() { + Collection> nodes = new ArrayList<>(); + for (EntryTreeNode node : root.values()) { + nodes.addAll(node.getNodesRecursively()); + } + return nodes.iterator(); + } + + @Override + public Stream> getAllEntries() { + return StreamSupport.stream(spliterator(), false) + .filter(EntryTreeNode::hasValue) + .map(EntryTreeNode::getEntry); + } + + @Override + public Stream> getRootNodes() { + return root.values().stream().map(Function.identity()); + } + + @Override + public boolean isEmpty() { + return root.isEmpty(); + } + + @Override + public HashEntryTree translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + HashEntryTree translatedTree = new HashEntryTree<>(); + for (EntryTreeNode node : this) { + translatedTree.insert(translator.translate(node.getEntry()), node.getValue()); + } + return translatedTree; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/HashTreeNode.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/HashTreeNode.java new file mode 100644 index 0000000..0a990bd --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/tree/HashTreeNode.java @@ -0,0 +1,75 @@ +package cuchaz.enigma.translation.mapping.tree; + +import cuchaz.enigma.translation.representation.entry.Entry; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class HashTreeNode implements EntryTreeNode, Iterable> { + private final Entry entry; + private final Map, HashTreeNode> children = new HashMap<>(); + private T value; + + HashTreeNode(Entry entry) { + this.entry = entry; + } + + void putValue(T value) { + this.value = value; + } + + T removeValue() { + T value = this.value; + this.value = null; + return value; + } + + @Nullable + HashTreeNode getChild(Entry entry) { + return children.get(entry); + } + + @Nonnull + HashTreeNode computeChild(Entry entry) { + return children.computeIfAbsent(entry, HashTreeNode::new); + } + + void remove(Entry entry) { + children.remove(entry); + } + + @Override + @Nullable + public T getValue() { + return value; + } + + @Override + public Entry getEntry() { + return entry; + } + + @Override + public boolean isEmpty() { + return children.isEmpty() && value == null; + } + + @Override + public Collection> getChildren() { + return children.keySet(); + } + + @Override + public Collection> getChildNodes() { + return children.values(); + } + + @Override + public Iterator> iterator() { + return children.values().iterator(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/AccessFlags.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/AccessFlags.java new file mode 100644 index 0000000..b280eef --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/AccessFlags.java @@ -0,0 +1,116 @@ +package cuchaz.enigma.translation.representation; + +import cuchaz.enigma.analysis.Access; +import org.objectweb.asm.Opcodes; + +import java.lang.reflect.Modifier; + +public class AccessFlags { + public static final AccessFlags PRIVATE = new AccessFlags(Opcodes.ACC_PRIVATE); + public static final AccessFlags PUBLIC = new AccessFlags(Opcodes.ACC_PUBLIC); + + private int flags; + + public AccessFlags(int flags) { + this.flags = flags; + } + + public boolean isPrivate() { + return Modifier.isPrivate(this.flags); + } + + public boolean isProtected() { + return Modifier.isProtected(this.flags); + } + + public boolean isPublic() { + return Modifier.isPublic(this.flags); + } + + public boolean isSynthetic() { + return (this.flags & Opcodes.ACC_SYNTHETIC) != 0; + } + + public boolean isStatic() { + return Modifier.isStatic(this.flags); + } + + public boolean isEnum() { + return (flags & Opcodes.ACC_ENUM) != 0; + } + + public boolean isBridge() { + return (flags & Opcodes.ACC_BRIDGE) != 0; + } + + public boolean isFinal() { + return (flags & Opcodes.ACC_FINAL) != 0; + } + + public boolean isInterface() { + return (flags & Opcodes.ACC_INTERFACE) != 0; + } + + public AccessFlags setPrivate() { + this.setVisibility(Opcodes.ACC_PRIVATE); + return this; + } + + public AccessFlags setProtected() { + this.setVisibility(Opcodes.ACC_PROTECTED); + return this; + } + + public AccessFlags setPublic() { + this.setVisibility(Opcodes.ACC_PUBLIC); + return this; + } + + public AccessFlags setBridge() { + flags |= Opcodes.ACC_BRIDGE; + return this; + } + + @Deprecated + public AccessFlags setBridged() { + return setBridge(); + } + + public void setVisibility(int visibility) { + this.resetVisibility(); + this.flags |= visibility; + } + + private void resetVisibility() { + this.flags &= ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC); + } + + public int getFlags() { + return this.flags; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof AccessFlags && ((AccessFlags) obj).flags == flags; + } + + @Override + public int hashCode() { + return flags; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(Access.get(this).toString().toLowerCase()); + if (isStatic()) { + builder.append(" static"); + } + if (isSynthetic()) { + builder.append(" synthetic"); + } + if (isBridge()) { + builder.append(" bridge"); + } + return builder.toString(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/Lambda.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/Lambda.java new file mode 100644 index 0000000..ad9389c --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/Lambda.java @@ -0,0 +1,105 @@ +package cuchaz.enigma.translation.representation; + +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import cuchaz.enigma.translation.representation.entry.ParentedEntry; + +import java.util.Objects; + +public class Lambda implements Translatable { + private final String invokedName; + private final MethodDescriptor invokedType; + private final MethodDescriptor samMethodType; + private final ParentedEntry implMethod; + private final MethodDescriptor instantiatedMethodType; + + public Lambda(String invokedName, MethodDescriptor invokedType, MethodDescriptor samMethodType, ParentedEntry implMethod, MethodDescriptor instantiatedMethodType) { + this.invokedName = invokedName; + this.invokedType = invokedType; + this.samMethodType = samMethodType; + this.implMethod = implMethod; + this.instantiatedMethodType = instantiatedMethodType; + } + + @Override + public Lambda translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + MethodEntry samMethod = new MethodEntry(getInterface(), invokedName, samMethodType); + EntryMapping samMethodMapping = resolveMapping(resolver, mappings, samMethod); + + return new Lambda( + samMethodMapping != null ? samMethodMapping.getTargetName() : invokedName, + invokedType.translate(translator, resolver, mappings), + samMethodType.translate(translator, resolver, mappings), + implMethod.translate(translator, resolver, mappings), + instantiatedMethodType.translate(translator, resolver, mappings) + ); + } + + private EntryMapping resolveMapping(EntryResolver resolver, EntryMap mappings, MethodEntry methodEntry) { + for (MethodEntry entry : resolver.resolveEntry(methodEntry, ResolutionStrategy.RESOLVE_ROOT)) { + EntryMapping mapping = mappings.get(entry); + if (mapping != null) { + return mapping; + } + } + return null; + } + + public ClassEntry getInterface() { + return invokedType.getReturnDesc().getTypeEntry(); + } + + public String getInvokedName() { + return invokedName; + } + + public MethodDescriptor getInvokedType() { + return invokedType; + } + + public MethodDescriptor getSamMethodType() { + return samMethodType; + } + + public ParentedEntry getImplMethod() { + return implMethod; + } + + public MethodDescriptor getInstantiatedMethodType() { + return instantiatedMethodType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Lambda lambda = (Lambda) o; + return Objects.equals(invokedName, lambda.invokedName) && + Objects.equals(invokedType, lambda.invokedType) && + Objects.equals(samMethodType, lambda.samMethodType) && + Objects.equals(implMethod, lambda.implMethod) && + Objects.equals(instantiatedMethodType, lambda.instantiatedMethodType); + } + + @Override + public int hashCode() { + return Objects.hash(invokedName, invokedType, samMethodType, implMethod, instantiatedMethodType); + } + + @Override + public String toString() { + return "Lambda{" + + "invokedName='" + invokedName + '\'' + + ", invokedType=" + invokedType + + ", samMethodType=" + samMethodType + + ", implMethod=" + implMethod + + ", instantiatedMethodType=" + instantiatedMethodType + + '}'; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/MethodDescriptor.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/MethodDescriptor.java new file mode 100644 index 0000000..03c8686 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/MethodDescriptor.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.representation; + +import com.google.common.collect.Lists; +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.representation.entry.ClassEntry; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +public class MethodDescriptor implements Translatable { + + private List argumentDescs; + private TypeDescriptor returnDesc; + + public MethodDescriptor(String desc) { + try { + this.argumentDescs = Lists.newArrayList(); + int i = 0; + while (i < desc.length()) { + char c = desc.charAt(i); + if (c == '(') { + assert (this.argumentDescs.isEmpty()); + assert (this.returnDesc == null); + i++; + } else if (c == ')') { + i++; + break; + } else { + String type = TypeDescriptor.parseFirst(desc.substring(i)); + this.argumentDescs.add(new TypeDescriptor(type)); + i += type.length(); + } + } + this.returnDesc = new TypeDescriptor(TypeDescriptor.parseFirst(desc.substring(i))); + } catch (Exception ex) { + throw new IllegalArgumentException("Unable to parse method descriptor: " + desc, ex); + } + } + + public MethodDescriptor(List argumentDescs, TypeDescriptor returnDesc) { + this.argumentDescs = argumentDescs; + this.returnDesc = returnDesc; + } + + public List getArgumentDescs() { + return this.argumentDescs; + } + + public TypeDescriptor getReturnDesc() { + return this.returnDesc; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("("); + for (TypeDescriptor desc : this.argumentDescs) { + buf.append(desc); + } + buf.append(")"); + buf.append(this.returnDesc); + return buf.toString(); + } + + public Iterable types() { + List descs = Lists.newArrayList(); + descs.addAll(this.argumentDescs); + descs.add(this.returnDesc); + return descs; + } + + @Override + public boolean equals(Object other) { + return other instanceof MethodDescriptor && equals((MethodDescriptor) other); + } + + public boolean equals(MethodDescriptor other) { + return this.argumentDescs.equals(other.argumentDescs) && this.returnDesc.equals(other.returnDesc); + } + + @Override + public int hashCode() { + return Objects.hash(this.argumentDescs.hashCode(), this.returnDesc.hashCode()); + } + + public boolean hasClass(ClassEntry classEntry) { + for (TypeDescriptor desc : types()) { + if (desc.containsType() && desc.getTypeEntry().equals(classEntry)) { + return true; + } + } + return false; + } + + public MethodDescriptor remap(Function remapper) { + List argumentDescs = new ArrayList<>(this.argumentDescs.size()); + for (TypeDescriptor desc : this.argumentDescs) { + argumentDescs.add(desc.remap(remapper)); + } + return new MethodDescriptor(argumentDescs, returnDesc.remap(remapper)); + } + + @Override + public MethodDescriptor translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + List translatedArguments = new ArrayList<>(argumentDescs.size()); + for (TypeDescriptor argument : argumentDescs) { + translatedArguments.add(translator.translate(argument)); + } + return new MethodDescriptor(translatedArguments, translator.translate(returnDesc)); + } + + public boolean canConflictWith(MethodDescriptor descriptor) { + return descriptor.argumentDescs.equals(argumentDescs); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/Signature.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/Signature.java new file mode 100644 index 0000000..424088a --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/Signature.java @@ -0,0 +1,98 @@ +package cuchaz.enigma.translation.representation; + +import cuchaz.enigma.bytecode.translators.TranslationSignatureVisitor; +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; +import org.objectweb.asm.signature.SignatureWriter; + +import java.util.function.Function; +import java.util.regex.Pattern; + +public class Signature implements Translatable { + private static final Pattern OBJECT_PATTERN = Pattern.compile(".*:Ljava/lang/Object;:.*"); + + private final String signature; + private final boolean isType; + + private Signature(String signature, boolean isType) { + if (signature != null && OBJECT_PATTERN.matcher(signature).matches()) { + signature = signature.replaceAll(":Ljava/lang/Object;:", "::"); + } + + this.signature = signature; + this.isType = isType; + } + + public static Signature createTypedSignature(String signature) { + if (signature != null && !signature.isEmpty()) { + return new Signature(signature, true); + } + return new Signature(null, true); + } + + public static Signature createSignature(String signature) { + if (signature != null && !signature.isEmpty()) { + return new Signature(signature, false); + } + return new Signature(null, false); + } + + public String getSignature() { + return signature; + } + + public boolean isType() { + return isType; + } + + public Signature remap(Function remapper) { + if (signature == null) { + return this; + } + SignatureWriter writer = new SignatureWriter(); + SignatureVisitor visitor = new TranslationSignatureVisitor(remapper, writer); + if (isType) { + new SignatureReader(signature).acceptType(visitor); + } else { + new SignatureReader(signature).accept(visitor); + } + return new Signature(writer.toString(), isType); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Signature) { + Signature other = (Signature) obj; + return (other.signature == null && signature == null || other.signature != null + && signature != null && other.signature.equals(signature)) + && other.isType == this.isType; + } + return false; + } + + @Override + public int hashCode() { + int hash = (isType ? 1 : 0) << 16; + if (signature != null) { + hash |= signature.hashCode(); + } + + return hash; + } + + @Override + public String toString() { + return signature; + } + + @Override + public Translatable translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + return remap(name -> translator.translate(new ClassEntry(name)).getFullName()); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/TypeDescriptor.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/TypeDescriptor.java new file mode 100644 index 0000000..f7ba849 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/TypeDescriptor.java @@ -0,0 +1,268 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.representation; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.representation.entry.ClassEntry; + +import java.util.Map; +import java.util.function.Function; + +public class TypeDescriptor implements Translatable { + + protected final String desc; + + public TypeDescriptor(String desc) { + Preconditions.checkNotNull(desc, "Desc cannot be null"); + + // don't deal with generics + // this is just for raw jvm types + if (desc.charAt(0) == 'T' || desc.indexOf('<') >= 0 || desc.indexOf('>') >= 0) { + throw new IllegalArgumentException("don't use with generic types or templates: " + desc); + } + + this.desc = desc; + } + + public static String parseFirst(String in) { + + if (in == null || in.length() <= 0) { + throw new IllegalArgumentException("No desc to parse, input is empty!"); + } + + // read one desc from the input + + char c = in.charAt(0); + + // first check for void + if (c == 'V') { + return "V"; + } + + // then check for primitives + Primitive primitive = Primitive.get(c); + if (primitive != null) { + return in.substring(0, 1); + } + + // then check for classes + if (c == 'L') { + return readClass(in); + } + + // then check for templates + if (c == 'T') { + return readClass(in); + } + + // then check for arrays + int dim = countArrayDimension(in); + if (dim > 0) { + String arrayType = TypeDescriptor.parseFirst(in.substring(dim)); + return in.substring(0, dim + arrayType.length()); + } + + throw new IllegalArgumentException("don't know how to parse: " + in); + } + + private static int countArrayDimension(String in) { + int i = 0; + while (i < in.length() && in.charAt(i) == '[') + i++; + return i; + } + + private static String readClass(String in) { + // read all the characters in the buffer until we hit a ';' + // include the parameters too + StringBuilder buf = new StringBuilder(); + int depth = 0; + for (int i = 0; i < in.length(); i++) { + char c = in.charAt(i); + buf.append(c); + + if (c == '<') { + depth++; + } else if (c == '>') { + depth--; + } else if (depth == 0 && c == ';') { + return buf.toString(); + } + } + return null; + } + + public static TypeDescriptor of(String name) { + return new TypeDescriptor("L" + name + ";"); + } + + @Override + public String toString() { + return this.desc; + } + + public boolean isVoid() { + return this.desc.length() == 1 && this.desc.charAt(0) == 'V'; + } + + public boolean isPrimitive() { + return this.desc.length() == 1 && Primitive.get(this.desc.charAt(0)) != null; + } + + public Primitive getPrimitive() { + if (!isPrimitive()) { + throw new IllegalStateException("not a primitive"); + } + return Primitive.get(this.desc.charAt(0)); + } + + public boolean isType() { + return this.desc.charAt(0) == 'L' && this.desc.charAt(this.desc.length() - 1) == ';'; + } + + public ClassEntry getTypeEntry() { + if (isType()) { + String name = this.desc.substring(1, this.desc.length() - 1); + + int pos = name.indexOf('<'); + if (pos >= 0) { + // remove the parameters from the class name + name = name.substring(0, pos); + } + + return new ClassEntry(name); + + } else if (isArray() && getArrayType().isType()) { + return getArrayType().getTypeEntry(); + } else { + throw new IllegalStateException("desc doesn't have a class"); + } + } + + public boolean isArray() { + return this.desc.charAt(0) == '['; + } + + public int getArrayDimension() { + if (!isArray()) { + throw new IllegalStateException("not an array"); + } + return countArrayDimension(this.desc); + } + + public TypeDescriptor getArrayType() { + if (!isArray()) { + throw new IllegalStateException("not an array"); + } + return new TypeDescriptor(this.desc.substring(getArrayDimension())); + } + + public boolean containsType() { + return isType() || (isArray() && getArrayType().containsType()); + } + + @Override + public boolean equals(Object other) { + return other instanceof TypeDescriptor && equals((TypeDescriptor) other); + } + + public boolean equals(TypeDescriptor other) { + return this.desc.equals(other.desc); + } + + @Override + public int hashCode() { + return this.desc.hashCode(); + } + + public TypeDescriptor remap(Function remapper) { + String desc = this.desc; + if (isType() || (isArray() && containsType())) { + String replacedName = remapper.apply(this.getTypeEntry().getFullName()); + if (replacedName != null) { + if (this.isType()) { + desc = "L" + replacedName + ";"; + } else { + desc = getArrayPrefix(this.getArrayDimension()) + "L" + replacedName + ";"; + } + } + } + return new TypeDescriptor(desc); + } + + private static String getArrayPrefix(int dimension) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < dimension; i++) { + buf.append("["); + } + return buf.toString(); + } + + public int getSize() { + switch (desc.charAt(0)) { + case 'J': + case 'D': + if (desc.length() == 1) { + return 2; + } else { + return 1; + } + default: + return 1; + } + } + + @Override + public Translatable translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + return remap(name -> translator.translate(new ClassEntry(name)).getFullName()); + } + + public enum Primitive { + BYTE('B'), + CHARACTER('C'), + SHORT('S'), + INTEGER('I'), + LONG('J'), + FLOAT('F'), + DOUBLE('D'), + BOOLEAN('Z'); + + private static final Map lookup; + + static { + lookup = Maps.newTreeMap(); + for (Primitive val : values()) { + lookup.put(val.getCode(), val); + } + } + + private char code; + + Primitive(char code) { + this.code = code; + } + + public static Primitive get(char code) { + return lookup.get(code); + } + + public char getCode() { + return this.code; + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassDefEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassDefEntry.java new file mode 100644 index 0000000..6930765 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassDefEntry.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.Signature; + +import javax.annotation.Nullable; +import java.util.Arrays; + +public class ClassDefEntry extends ClassEntry implements DefEntry { + private final AccessFlags access; + private final Signature signature; + private final ClassEntry superClass; + private final ClassEntry[] interfaces; + + public ClassDefEntry(String className, Signature signature, AccessFlags access, @Nullable ClassEntry superClass, ClassEntry[] interfaces) { + this(getOuterClass(className), getInnerName(className), signature, access, superClass, interfaces, null); + } + + public ClassDefEntry(ClassEntry parent, String className, Signature signature, AccessFlags access, @Nullable ClassEntry superClass, ClassEntry[] interfaces) { + this(parent, className, signature, access, superClass, interfaces, null); + } + + public ClassDefEntry(ClassEntry parent, String className, Signature signature, AccessFlags access, @Nullable ClassEntry superClass, + ClassEntry[] interfaces, String javadocs) { + super(parent, className, javadocs); + Preconditions.checkNotNull(signature, "Class signature cannot be null"); + Preconditions.checkNotNull(access, "Class access cannot be null"); + + this.signature = signature; + this.access = access; + this.superClass = superClass; + this.interfaces = interfaces != null ? interfaces : new ClassEntry[0]; + } + + public static ClassDefEntry parse(int access, String name, String signature, String superName, String[] interfaces) { + ClassEntry superClass = superName != null ? new ClassEntry(superName) : null; + ClassEntry[] interfaceClasses = Arrays.stream(interfaces).map(ClassEntry::new).toArray(ClassEntry[]::new); + return new ClassDefEntry(name, Signature.createSignature(signature), new AccessFlags(access), superClass, interfaceClasses); + } + + public Signature getSignature() { + return signature; + } + + @Override + public AccessFlags getAccess() { + return access; + } + + @Nullable + public ClassEntry getSuperClass() { + return superClass; + } + + public ClassEntry[] getInterfaces() { + return interfaces; + } + + @Override + public ClassDefEntry translate(Translator translator, @Nullable EntryMapping mapping) { + Signature translatedSignature = translator.translate(signature); + String translatedName = mapping != null ? mapping.getTargetName() : name; + AccessFlags translatedAccess = mapping != null ? mapping.getAccessModifier().transform(access) : access; + ClassEntry translatedSuper = translator.translate(superClass); + ClassEntry[] translatedInterfaces = Arrays.stream(interfaces).map(translator::translate).toArray(ClassEntry[]::new); + String docs = mapping != null ? mapping.getJavadoc() : null; + return new ClassDefEntry(parent, translatedName, translatedSignature, translatedAccess, translatedSuper, translatedInterfaces, docs); + } + + @Override + public ClassDefEntry withName(String name) { + return new ClassDefEntry(parent, name, signature, access, superClass, interfaces, javadocs); + } + + @Override + public ClassDefEntry withParent(ClassEntry parent) { + return new ClassDefEntry(parent, name, signature, access, superClass, interfaces, javadocs); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java new file mode 100644 index 0000000..7d4b2ba --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java @@ -0,0 +1,214 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.representation.entry; + +import cuchaz.enigma.translation.mapping.IllegalNameException; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.NameValidator; +import cuchaz.enigma.translation.representation.TypeDescriptor; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; + +public class ClassEntry extends ParentedEntry implements Comparable { + private final String fullName; + + public ClassEntry(String className) { + this(getOuterClass(className), getInnerName(className), null); + } + + public ClassEntry(@Nullable ClassEntry parent, String className) { + this(parent, className, null); + } + + public ClassEntry(@Nullable ClassEntry parent, String className, @Nullable String javadocs) { + super(parent, className, javadocs); + if (parent != null) { + fullName = parent.getFullName() + "$" + name; + } else { + fullName = name; + } + + if (parent == null && className.indexOf('.') >= 0) { + throw new IllegalArgumentException("Class name must be in JVM format. ie, path/to/package/class$inner : " + className); + } + } + + @Override + public Class getParentType() { + return ClassEntry.class; + } + + @Override + public String getName() { + return this.name; + } + + public String getFullName() { + return fullName; + } + + @Override + public ClassEntry translate(Translator translator, @Nullable EntryMapping mapping) { + if (name.charAt(0) == '[') { + String translatedName = translator.translate(new TypeDescriptor(name)).toString(); + return new ClassEntry(parent, translatedName); + } + + String translatedName = mapping != null ? mapping.getTargetName() : name; + String docs = mapping != null ? mapping.getJavadoc() : null; + return new ClassEntry(parent, translatedName, docs); + } + + @Override + public ClassEntry getContainingClass() { + return this; + } + + @Override + public int hashCode() { + return fullName.hashCode(); + } + + @Override + public boolean equals(Object other) { + return other instanceof ClassEntry && equals((ClassEntry) other); + } + + public boolean equals(ClassEntry other) { + return other != null && Objects.equals(parent, other.parent) && this.name.equals(other.name); + } + + @Override + public boolean canConflictWith(Entry entry) { + return true; + } + + @Override + public void validateName(String name) throws IllegalNameException { + NameValidator.validateClassName(name); + } + + @Override + public ClassEntry withName(String name) { + return new ClassEntry(parent, name, javadocs); + } + + @Override + public ClassEntry withParent(ClassEntry parent) { + return new ClassEntry(parent, name, javadocs); + } + + @Override + public String toString() { + return getFullName(); + } + + public String getPackageName() { + return getPackageName(fullName); + } + + public String getSimpleName() { + int packagePos = name.lastIndexOf('/'); + if (packagePos > 0) { + return name.substring(packagePos + 1); + } + return name; + } + + public boolean isInnerClass() { + return parent != null; + } + + @Nullable + public ClassEntry getOuterClass() { + return parent; + } + + @Nonnull + public ClassEntry getOutermostClass() { + if (parent == null) { + return this; + } + return parent.getOutermostClass(); + } + + public ClassEntry buildClassEntry(List classChain) { + assert (classChain.contains(this)); + StringBuilder buf = new StringBuilder(); + for (ClassEntry chainEntry : classChain) { + if (buf.length() == 0) { + buf.append(chainEntry.getFullName()); + } else { + buf.append("$"); + buf.append(chainEntry.getSimpleName()); + } + + if (chainEntry == this) { + break; + } + } + return new ClassEntry(buf.toString()); + } + + public boolean isJre() { + String packageName = getPackageName(); + return packageName != null && (packageName.startsWith("java") || packageName.startsWith("javax")); + } + + public static String getPackageName(String name) { + int pos = name.lastIndexOf('/'); + if (pos > 0) { + return name.substring(0, pos); + } + return null; + } + + @Nullable + public static ClassEntry getOuterClass(String name) { + int index = name.lastIndexOf('$'); + if (index >= 0) { + return new ClassEntry(name.substring(0, index)); + } + return null; + } + + public static String getInnerName(String name) { + int innerClassPos = name.lastIndexOf('$'); + if (innerClassPos > 0) { + return name.substring(innerClassPos + 1); + } + return name; + } + + @Override + public String getSourceRemapName() { + ClassEntry outerClass = getOuterClass(); + if (outerClass != null) { + return outerClass.getSourceRemapName() + "." + name; + } + return getSimpleName(); + } + + @Override + public int compareTo(ClassEntry entry) { + String fullName = getFullName(); + String otherFullName = entry.getFullName(); + if (fullName.length() != otherFullName.length()) { + return fullName.length() - otherFullName.length(); + } + return fullName.compareTo(otherFullName); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/DefEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/DefEntry.java new file mode 100644 index 0000000..82536c7 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/DefEntry.java @@ -0,0 +1,7 @@ +package cuchaz.enigma.translation.representation.entry; + +import cuchaz.enigma.translation.representation.AccessFlags; + +public interface DefEntry

> extends Entry

{ + AccessFlags getAccess(); +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java new file mode 100644 index 0000000..40bff31 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.representation.entry; + +import cuchaz.enigma.translation.mapping.IllegalNameException; +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.mapping.NameValidator; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +public interface Entry

> extends Translatable { + String getName(); + + String getJavadocs(); + + default String getSourceRemapName() { + return getName(); + } + + @Nullable + P getParent(); + + Class

getParentType(); + + Entry

withName(String name); + + Entry

withParent(P parent); + + boolean canConflictWith(Entry entry); + + @Nullable + default ClassEntry getContainingClass() { + P parent = getParent(); + if (parent == null) { + return null; + } + if (parent instanceof ClassEntry) { + return (ClassEntry) parent; + } + return parent.getContainingClass(); + } + + default List> getAncestry() { + P parent = getParent(); + List> entries = new ArrayList<>(); + if (parent != null) { + entries.addAll(parent.getAncestry()); + } + entries.add(this); + return entries; + } + + @Nullable + @SuppressWarnings("unchecked") + default > E findAncestor(Class type) { + List> ancestry = getAncestry(); + for (int i = ancestry.size() - 1; i >= 0; i--) { + Entry ancestor = ancestry.get(i); + if (type.isAssignableFrom(ancestor.getClass())) { + return (E) ancestor; + } + } + return null; + } + + @SuppressWarnings("unchecked") + default > Entry

replaceAncestor(E target, E replacement) { + if (replacement.equals(target)) { + return this; + } + + if (equals(target)) { + return (Entry

) replacement; + } + + P parent = getParent(); + if (parent == null) { + return this; + } + + return withParent((P) parent.replaceAncestor(target, replacement)); + } + + default void validateName(String name) throws IllegalNameException { + NameValidator.validateIdentifier(name); + } + + @SuppressWarnings("unchecked") + @Nullable + default > Entry castParent(Class parentType) { + if (parentType.equals(getParentType())) { + return (Entry) this; + } + return null; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/FieldDefEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/FieldDefEntry.java new file mode 100644 index 0000000..f9282b2 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/FieldDefEntry.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.Signature; +import cuchaz.enigma.translation.representation.TypeDescriptor; + +import javax.annotation.Nullable; + +public class FieldDefEntry extends FieldEntry implements DefEntry { + private final AccessFlags access; + private final Signature signature; + + public FieldDefEntry(ClassEntry owner, String name, TypeDescriptor desc, Signature signature, AccessFlags access) { + this(owner, name, desc, signature, access, null); + } + + public FieldDefEntry(ClassEntry owner, String name, TypeDescriptor desc, Signature signature, AccessFlags access, String javadocs) { + super(owner, name, desc, javadocs); + Preconditions.checkNotNull(access, "Field access cannot be null"); + Preconditions.checkNotNull(signature, "Field signature cannot be null"); + this.access = access; + this.signature = signature; + } + + public static FieldDefEntry parse(ClassEntry owner, int access, String name, String desc, String signature) { + return new FieldDefEntry(owner, name, new TypeDescriptor(desc), Signature.createTypedSignature(signature), new AccessFlags(access), null); + } + + @Override + public AccessFlags getAccess() { + return access; + } + + public Signature getSignature() { + return signature; + } + + @Override + public FieldDefEntry translate(Translator translator, @Nullable EntryMapping mapping) { + TypeDescriptor translatedDesc = translator.translate(desc); + Signature translatedSignature = translator.translate(signature); + String translatedName = mapping != null ? mapping.getTargetName() : name; + AccessFlags translatedAccess = mapping != null ? mapping.getAccessModifier().transform(access) : access; + String docs = mapping != null ? mapping.getJavadoc() : null; + return new FieldDefEntry(parent, translatedName, translatedDesc, translatedSignature, translatedAccess, docs); + } + + @Override + public FieldDefEntry withName(String name) { + return new FieldDefEntry(parent, name, desc, signature, access, javadocs); + } + + @Override + public FieldDefEntry withParent(ClassEntry owner) { + return new FieldDefEntry(owner, this.name, this.desc, signature, access, javadocs); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/FieldEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/FieldEntry.java new file mode 100644 index 0000000..0e47878 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/FieldEntry.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.TypeDescriptor; + +import javax.annotation.Nullable; +import java.util.Objects; + +public class FieldEntry extends ParentedEntry implements Comparable { + protected final TypeDescriptor desc; + + public FieldEntry(ClassEntry parent, String name, TypeDescriptor desc) { + this(parent, name, desc, null); + } + + public FieldEntry(ClassEntry parent, String name, TypeDescriptor desc, String javadocs) { + super(parent, name, javadocs); + + Preconditions.checkNotNull(parent, "Owner cannot be null"); + Preconditions.checkNotNull(desc, "Field descriptor cannot be null"); + + this.desc = desc; + } + + public static FieldEntry parse(String owner, String name, String desc) { + return new FieldEntry(new ClassEntry(owner), name, new TypeDescriptor(desc), null); + } + + @Override + public Class getParentType() { + return ClassEntry.class; + } + + public TypeDescriptor getDesc() { + return this.desc; + } + + @Override + public FieldEntry withName(String name) { + return new FieldEntry(parent, name, desc, null); + } + + @Override + public FieldEntry withParent(ClassEntry parent) { + return new FieldEntry(parent, this.name, this.desc, null); + } + + @Override + protected FieldEntry translate(Translator translator, @Nullable EntryMapping mapping) { + String translatedName = mapping != null ? mapping.getTargetName() : name; + String docs = mapping != null ? mapping.getJavadoc() : null; + return new FieldEntry(parent, translatedName, translator.translate(desc), docs); + } + + @Override + public int hashCode() { + return Objects.hash(this.parent, this.name, this.desc); + } + + @Override + public boolean equals(Object other) { + return other instanceof FieldEntry && equals((FieldEntry) other); + } + + public boolean equals(FieldEntry other) { + return this.parent.equals(other.parent) && name.equals(other.name) && desc.equals(other.desc); + } + + @Override + public boolean canConflictWith(Entry entry) { + return entry instanceof FieldEntry && ((FieldEntry) entry).parent.equals(parent); + } + + @Override + public String toString() { + return this.parent.getFullName() + "." + this.name + ":" + this.desc; + } + + @Override + public int compareTo(FieldEntry entry) { + return (name + desc.toString()).compareTo(entry.name + entry.desc.toString()); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableDefEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableDefEntry.java new file mode 100644 index 0000000..aad4236 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableDefEntry.java @@ -0,0 +1,51 @@ +package cuchaz.enigma.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.TypeDescriptor; + +import javax.annotation.Nullable; + +/** + * TypeDescriptor... + * Created by Thog + * 19/10/2016 + */ +public class LocalVariableDefEntry extends LocalVariableEntry { + protected final TypeDescriptor desc; + + public LocalVariableDefEntry(MethodEntry ownerEntry, int index, String name, boolean parameter, TypeDescriptor desc, String javadoc) { + super(ownerEntry, index, name, parameter, javadoc); + Preconditions.checkNotNull(desc, "Variable desc cannot be null"); + + this.desc = desc; + } + + public TypeDescriptor getDesc() { + return desc; + } + + @Override + public LocalVariableDefEntry translate(Translator translator, @Nullable EntryMapping mapping) { + TypeDescriptor translatedDesc = translator.translate(desc); + String translatedName = mapping != null ? mapping.getTargetName() : name; + String javadoc = mapping != null ? mapping.getJavadoc() : javadocs; + return new LocalVariableDefEntry(parent, index, translatedName, parameter, translatedDesc, javadoc); + } + + @Override + public LocalVariableDefEntry withName(String name) { + return new LocalVariableDefEntry(parent, index, name, parameter, desc, javadocs); + } + + @Override + public LocalVariableDefEntry withParent(MethodEntry entry) { + return new LocalVariableDefEntry(entry, index, name, parameter, desc, javadocs); + } + + @Override + public String toString() { + return this.parent + "(" + this.index + ":" + this.name + ":" + this.desc + ")"; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableEntry.java new file mode 100644 index 0000000..3f98f03 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/LocalVariableEntry.java @@ -0,0 +1,93 @@ +package cuchaz.enigma.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; + +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * TypeDescriptor... + * Created by Thog + * 19/10/2016 + */ +public class LocalVariableEntry extends ParentedEntry implements Comparable { + + protected final int index; + protected final boolean parameter; + + public LocalVariableEntry(MethodEntry parent, int index, String name, boolean parameter, String javadoc) { + super(parent, name, javadoc); + + Preconditions.checkNotNull(parent, "Variable owner cannot be null"); + Preconditions.checkArgument(index >= 0, "Index must be positive"); + + this.index = index; + this.parameter = parameter; + } + + @Override + public Class getParentType() { + return MethodEntry.class; + } + + public boolean isArgument() { + return this.parameter; + } + + public int getIndex() { + return index; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public LocalVariableEntry translate(Translator translator, @Nullable EntryMapping mapping) { + String translatedName = mapping != null ? mapping.getTargetName() : name; + String javadoc = mapping != null ? mapping.getJavadoc() : null; + return new LocalVariableEntry(parent, index, translatedName, parameter, javadoc); + } + + @Override + public LocalVariableEntry withName(String name) { + return new LocalVariableEntry(parent, index, name, parameter, javadocs); + } + + @Override + public LocalVariableEntry withParent(MethodEntry parent) { + return new LocalVariableEntry(parent, index, name, parameter, javadocs); + } + + @Override + public int hashCode() { + return Objects.hash(this.parent, this.index); + } + + @Override + public boolean equals(Object other) { + return other instanceof LocalVariableEntry && equals((LocalVariableEntry) other); + } + + public boolean equals(LocalVariableEntry other) { + return this.parent.equals(other.parent) && this.index == other.index; + } + + @Override + public boolean canConflictWith(Entry entry) { + return entry instanceof LocalVariableEntry && ((LocalVariableEntry) entry).parent.equals(parent); + } + + @Override + public String toString() { + return this.parent + "(" + this.index + ":" + this.name + ")"; + } + + @Override + public int compareTo(LocalVariableEntry entry) { + return Integer.compare(index, entry.index); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/MethodDefEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/MethodDefEntry.java new file mode 100644 index 0000000..4e75a5c --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/MethodDefEntry.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.Signature; + +import javax.annotation.Nullable; + +public class MethodDefEntry extends MethodEntry implements DefEntry { + private final AccessFlags access; + private final Signature signature; + + public MethodDefEntry(ClassEntry owner, String name, MethodDescriptor descriptor, Signature signature, AccessFlags access) { + this(owner, name, descriptor, signature, access, null); + } + + public MethodDefEntry(ClassEntry owner, String name, MethodDescriptor descriptor, Signature signature, AccessFlags access, String docs) { + super(owner, name, descriptor, docs); + Preconditions.checkNotNull(access, "Method access cannot be null"); + Preconditions.checkNotNull(signature, "Method signature cannot be null"); + this.access = access; + this.signature = signature; + } + + public static MethodDefEntry parse(ClassEntry owner, int access, String name, String desc, String signature) { + return new MethodDefEntry(owner, name, new MethodDescriptor(desc), Signature.createSignature(signature), new AccessFlags(access), null); + } + + @Override + public AccessFlags getAccess() { + return access; + } + + public Signature getSignature() { + return signature; + } + + @Override + public MethodDefEntry translate(Translator translator, @Nullable EntryMapping mapping) { + MethodDescriptor translatedDesc = translator.translate(descriptor); + Signature translatedSignature = translator.translate(signature); + String translatedName = mapping != null ? mapping.getTargetName() : name; + AccessFlags translatedAccess = mapping != null ? mapping.getAccessModifier().transform(access) : access; + String docs = mapping != null ? mapping.getJavadoc() : null; + return new MethodDefEntry(parent, translatedName, translatedDesc, translatedSignature, translatedAccess, docs); + } + + @Override + public MethodDefEntry withName(String name) { + return new MethodDefEntry(parent, name, descriptor, signature, access, javadocs); + } + + @Override + public MethodDefEntry withParent(ClassEntry parent) { + return new MethodDefEntry(new ClassEntry(parent.getFullName()), name, descriptor, signature, access, javadocs); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/MethodEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/MethodEntry.java new file mode 100644 index 0000000..086a5c1 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/MethodEntry.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.MethodDescriptor; + +import javax.annotation.Nullable; +import java.util.Objects; + +public class MethodEntry extends ParentedEntry implements Comparable { + + protected final MethodDescriptor descriptor; + + public MethodEntry(ClassEntry parent, String name, MethodDescriptor descriptor) { + this(parent, name, descriptor, null); + } + + public MethodEntry(ClassEntry parent, String name, MethodDescriptor descriptor, String javadocs) { + super(parent, name, javadocs); + + Preconditions.checkNotNull(parent, "Parent cannot be null"); + Preconditions.checkNotNull(descriptor, "Method descriptor cannot be null"); + + this.descriptor = descriptor; + } + + public static MethodEntry parse(String owner, String name, String desc) { + return new MethodEntry(new ClassEntry(owner), name, new MethodDescriptor(desc), null); + } + + @Override + public Class getParentType() { + return ClassEntry.class; + } + + public MethodDescriptor getDesc() { + return this.descriptor; + } + + public boolean isConstructor() { + return name.equals("") || name.equals(""); + } + + @Override + public MethodEntry translate(Translator translator, @Nullable EntryMapping mapping) { + String translatedName = mapping != null ? mapping.getTargetName() : name; + String docs = mapping != null ? mapping.getJavadoc() : null; + return new MethodEntry(parent, translatedName, translator.translate(descriptor), docs); + } + + @Override + public MethodEntry withName(String name) { + return new MethodEntry(parent, name, descriptor, javadocs); + } + + @Override + public MethodEntry withParent(ClassEntry parent) { + return new MethodEntry(new ClassEntry(parent.getFullName()), name, descriptor, javadocs); + } + + @Override + public int hashCode() { + return Objects.hash(this.parent, this.name, this.descriptor); + } + + @Override + public boolean equals(Object other) { + return other instanceof MethodEntry && equals((MethodEntry) other); + } + + public boolean equals(MethodEntry other) { + return this.parent.equals(other.getParent()) && this.name.equals(other.getName()) && this.descriptor.equals(other.getDesc()); + } + + @Override + public boolean canConflictWith(Entry entry) { + if (entry instanceof MethodEntry) { + MethodEntry methodEntry = (MethodEntry) entry; + return methodEntry.parent.equals(parent) && methodEntry.descriptor.canConflictWith(descriptor); + } + return false; + } + + @Override + public String toString() { + return this.parent.getFullName() + "." + this.name + this.descriptor; + } + + @Override + public int compareTo(MethodEntry entry) { + return (name + descriptor.toString()).compareTo(entry.name + entry.descriptor.toString()); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ParentedEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ParentedEntry.java new file mode 100644 index 0000000..95be22c --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ParentedEntry.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.representation.entry; + +import com.google.common.base.Preconditions; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMap; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; + +import javax.annotation.Nullable; + +public abstract class ParentedEntry

> implements Entry

{ + protected final P parent; + protected final String name; + protected final @Nullable String javadocs; + + protected ParentedEntry(P parent, String name, String javadocs) { + this.parent = parent; + this.name = name; + this.javadocs = javadocs; + + Preconditions.checkNotNull(name, "Name cannot be null"); + } + + @Override + public abstract ParentedEntry

withParent(P parent); + + @Override + public abstract ParentedEntry

withName(String name); + + protected abstract ParentedEntry

translate(Translator translator, @Nullable EntryMapping mapping); + + @Override + public String getName() { + return name; + } + + @Override + @Nullable + public P getParent() { + return parent; + } + + @Nullable + @Override + public String getJavadocs() { + return javadocs; + } + + @Override + public ParentedEntry

translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + P parent = getParent(); + EntryMapping mapping = resolveMapping(resolver, mappings); + if (parent == null) { + return translate(translator, mapping); + } + P translatedParent = translator.translate(parent); + return withParent(translatedParent).translate(translator, mapping); + } + + private EntryMapping resolveMapping(EntryResolver resolver, EntryMap mappings) { + for (ParentedEntry

entry : resolver.resolveEntry(this, ResolutionStrategy.RESOLVE_ROOT)) { + EntryMapping mapping = mappings.get(entry); + if (mapping != null) { + return mapping; + } + } + return null; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/utils/I18n.java b/enigma/src/main/java/cuchaz/enigma/utils/I18n.java new file mode 100644 index 0000000..e18532b --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/I18n.java @@ -0,0 +1,95 @@ +package cuchaz.enigma.utils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; +import com.google.common.reflect.ClassPath; +import com.google.common.reflect.ClassPath.ResourceInfo; +import com.google.gson.Gson; + +public class I18n { + public static final String DEFAULT_LANGUAGE = "en_us"; + private static final Gson GSON = new Gson(); + private static Map translations = Maps.newHashMap(); + private static Map defaultTranslations = Maps.newHashMap(); + private static Map languageNames = Maps.newHashMap(); + + static { + defaultTranslations = load(DEFAULT_LANGUAGE); + translations = defaultTranslations; + } + + @SuppressWarnings("unchecked") + public static Map load(String language) { + try (InputStream inputStream = I18n.class.getResourceAsStream("/lang/" + language + ".json")) { + if (inputStream != null) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + return GSON.fromJson(reader, Map.class); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return Collections.emptyMap(); + } + + public static String translate(String key) { + String value = translations.get(key); + if (value != null) { + return value; + } + value = defaultTranslations.get(key); + if (value != null) { + return value; + } + return key; + } + + public static String getLanguageName(String language) { + return languageNames.get(language); + } + + public static void setLanguage(String language) { + translations = load(language); + } + + public static ArrayList getAvailableLanguages() { + ArrayList list = new ArrayList(); + + try { + ImmutableList resources = ClassPath.from(Thread.currentThread().getContextClassLoader()).getResources().asList(); + Stream dirStream = resources.stream(); + dirStream.forEach(context -> { + String file = context.getResourceName(); + if (file.startsWith("lang/") && file.endsWith(".json")) { + String fileName = file.substring(5, file.length() - 5); + list.add(fileName); + loadLanguageName(fileName); + } + }); + } catch (IOException e) { + e.printStackTrace(); + } + return list; + } + + private static void loadLanguageName(String fileName) { + try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("lang/" + fileName + ".json")) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { + Map map = GSON.fromJson(reader, Map.class); + languageNames.put(fileName, map.get("language").toString()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/utils/Pair.java b/enigma/src/main/java/cuchaz/enigma/utils/Pair.java new file mode 100644 index 0000000..bf02cef --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/Pair.java @@ -0,0 +1,26 @@ +package cuchaz.enigma.utils; + +import java.util.Objects; + +public class Pair { + public final A a; + public final B b; + + public Pair(A a, B b) { + this.a = a; + this.b = b; + } + + @Override + public int hashCode() { + return Objects.hashCode(a) * 31 + + Objects.hashCode(b); + } + + @Override + public boolean equals(Object o) { + return o instanceof Pair && + Objects.equals(a, ((Pair) o).a) && + Objects.equals(b, ((Pair) o).b); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/utils/Utils.java b/enigma/src/main/java/cuchaz/enigma/utils/Utils.java new file mode 100644 index 0000000..2664099 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/Utils.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + *

+ * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.utils; + +import com.google.common.io.CharStreams; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class Utils { + public static String readStreamToString(InputStream in) throws IOException { + return CharStreams.toString(new InputStreamReader(in, StandardCharsets.UTF_8)); + } + + public static String readResourceToString(String path) throws IOException { + InputStream in = Utils.class.getResourceAsStream(path); + if (in == null) { + throw new IllegalArgumentException("Resource not found! " + path); + } + return readStreamToString(in); + } + + public static void delete(Path path) throws IOException { + if (Files.exists(path)) { + for (Path p : Files.walk(path).sorted(Comparator.reverseOrder()).collect(Collectors.toList())) { + Files.delete(p); + } + } + } + + public static byte[] zipSha1(Path path) throws IOException { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + // Algorithm guaranteed to be supported + throw new RuntimeException(e); + } + try (ZipFile zip = new ZipFile(path.toFile())) { + List entries = Collections.list(zip.entries()); + // only compare classes (some implementations may not generate directory entries) + entries.removeIf(entry -> !entry.getName().toLowerCase(Locale.ROOT).endsWith(".class")); + // different implementations may add zip entries in a different order + entries.sort(Comparator.comparing(ZipEntry::getName)); + byte[] buffer = new byte[8192]; + for (ZipEntry entry : entries) { + digest.update(entry.getName().getBytes(StandardCharsets.UTF_8)); + try (InputStream in = zip.getInputStream(entry)) { + int n; + while ((n = in.read(buffer)) != -1) { + digest.update(buffer, 0, n); + } + } + } + } + return digest.digest(); + } + + public static boolean isBlank(String input) { + if (input == null) { + return true; + } + for (int i = 0; i < input.length(); i++) { + if (!Character.isWhitespace(input.charAt(i))) { + return false; + } + } + return true; + } +} diff --git a/enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin b/enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin new file mode 100644 index 0000000..136a3e7 --- /dev/null +++ b/enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin @@ -0,0 +1 @@ +cuchaz.enigma.analysis.BuiltinPlugin diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json new file mode 100644 index 0000000..dbf4b93 --- /dev/null +++ b/enigma/src/main/resources/lang/en_us.json @@ -0,0 +1,164 @@ +{ + "language": "English", + + "mapping_format.enigma_file": "Enigma File", + "mapping_format.enigma_directory": "Enigma Directory", + "mapping_format.enigma_zip": "Enigma ZIP", + "mapping_format.tiny_v2": "Tiny v2", + "mapping_format.tiny_file": "Tiny File", + "mapping_format.srg_file": "SRG File", + "mapping_format.proguard": "Proguard", + "type.methods": "Methods", + "type.fields": "Fields", + "type.parameters": "Parameters", + "type.classes": "Classes", + + "menu.file": "File", + "menu.file.jar.open": "Open Jar...", + "menu.file.jar.close": "Close Jar", + "menu.file.mappings.open": "Open Mappings...", + "menu.file.mappings.save": "Save Mappings", + "menu.file.mappings.save_as": "Save Mappings As...", + "menu.file.mappings.close": "Close Mappings", + "menu.file.mappings.drop": "Drop Invalid Mappings", + "menu.file.export.source": "Export Source...", + "menu.file.export.jar": "Export Jar...", + "menu.file.stats": "Mapping Stats...", + "menu.file.stats.title": "Choose Included Members", + "menu.file.stats.generate": "Generate Stats", + "menu.file.exit": "Exit", + "menu.decompiler": "Decompiler", + "menu.view": "View", + "menu.view.themes": "Themes", + "menu.view.themes.default": "Default", + "menu.view.themes.darcula": "Darcula", + "menu.view.themes.system": "System", + "menu.view.themes.none": "None (JVM Default)", + "menu.view.languages": "Languages", + "menu.view.scale": "Scale", + "menu.view.scale.custom": "Custom...", + "menu.view.scale.custom.title": "Custom Scale", + "menu.view.change.title": "Changes", + "menu.view.change.summary": "Changes will be applied after the next restart.", + "menu.view.change.ok": "Ok", + "menu.view.search": "Search", + "menu.collab": "Collab", + "menu.collab.connect": "Connect to server", + "menu.collab.connect.error": "Error connecting to server", + "menu.collab.disconnect": "Disconnect", + "menu.collab.server.start": "Start server", + "menu.collab.server.start.error": "Error starting server", + "menu.collab.server.stop": "Stop server", + "menu.help": "Help", + "menu.help.about": "About", + "menu.help.about.title": "%s - About", + "menu.help.about.ok": "Ok", + "menu.help.github": "Github Page", + + "popup_menu.rename": "Rename", + "popup_menu.javadoc": "Edit Javadoc", + "popup_menu.inheritance": "Show Inheritance", + "popup_menu.implementations": "Show Implementations", + "popup_menu.calls": "Show Calls (All Implementations)", + "popup_menu.calls.specific": "Show Calls (Specific)", + "popup_menu.declaration": "Go to Declaration", + "popup_menu.back": "Go back", + "popup_menu.forward": "Go forward", + "popup_menu.mark_deobfuscated": "Mark as deobfuscated", + "popup_menu.reset_obfuscated": "Reset to obfuscated", + "popup_menu.zoom.in": "Zoom in", + "popup_menu.zoom.out": "Zoom out", + "popup_menu.zoom.reset": "Reset zoom", + + "info_panel.classes.obfuscated": "Obfuscated Classes", + "info_panel.classes.deobfuscated": "De-obfuscated Classes", + "info_panel.identifier": "Identifier Info", + "info_panel.identifier.none": "No identifier selected", + "info_panel.identifier.variable": "Variable", + "info_panel.identifier.field": "Field", + "info_panel.identifier.method": "Method", + "info_panel.identifier.constructor": "Constructor", + "info_panel.identifier.class": "Class", + "info_panel.identifier.type_descriptor": "TypeDescriptor", + "info_panel.identifier.method_descriptor": "MethodDescriptor", + "info_panel.identifier.modifier": "Modifier", + "info_panel.identifier.index": "Index", + "info_panel.editor.class.decompiling": "(decompiling...)", + "info_panel.editor.class.not_found": "Unable to find class:", + "info_panel.tree.inheritance": "Inheritance", + "info_panel.tree.implementations": "Implementations", + "info_panel.tree.calls": "Call Graph", + + "log_panel.messages": "Messages", + "log_panel.users": "Users", + + "progress.operation": "%s - Operation in progress", + "progress.jar.indexing": "Indexing jar", + "progress.jar.indexing.entries": "Entries...", + "progress.jar.indexing.references": "Entry references...", + "progress.jar.indexing.methods": "Bridge methods...", + "progress.jar.indexing.process": "Processing...", + "progress.jar.writing": "Writing jar...", + "progress.sources.writing": "Writing sources...", + "progress.classes.deobfuscating": "Deobfuscating classes...", + "progress.classes.decompiling": "Decompiling classes...", + "progress.mappings.enigma_file.loading": "Loading mapping file", + "progress.mappings.enigma_file.done": "Done!", + "progress.mappings.enigma_file.writing": "Writing classes", + "progress.mappings.enigma_directory.loading": "Loading mapping files", + "progress.mappings.enigma_directory.writing": "Writing classes", + "progress.mappings.tiny_file.loading": "Loading mapping file", + "progress.mappings.tiny_v2.loading": "Loading mapping file", + "progress.mappings.srg_file.generating": "Generating mappings", + "progress.mappings.srg_file.writing": "Writing mappings", + "progress.stats": "Generating stats", + "progress.stats.data": "Generating data", + + "javadocs.edit": "Edit Javadocs", + "javadocs.instruction": "Edit javadocs here.", + "javadocs.cancel": "Cancel", + "javadocs.save": "Save", + + "prompt.close.title": "Save your changes?", + "prompt.close.summary": "Your mappings have not been saved yet. Do you want to save?", + "prompt.close.save": "Save and close", + "prompt.close.discard": "Discard changes", + "prompt.close.cancel": "Cancel", + "prompt.open": "Open", + "prompt.cancel": "Cancel", + "prompt.connect.title": "Connect to server", + "prompt.connect.username": "Username", + "prompt.connect.ip": "IP", + "prompt.port": "Port", + "prompt.port.nan": "Port is not a number", + "prompt.port.invalid": "Port is out of range, should be between 0-65535", + "prompt.password": "Password", + "prompt.password.too_long": "Password is too long, it must be at most 255 characters.", + "prompt.create_server.title": "Create server", + + "disconnect.disconnected": "Disconnected", + "disconnect.server_closed": "Server closed", + "disconnect.wrong_jar": "Jar checksums don't match (you have the wrong jar)!", + "disconnect.wrong_password": "Incorrect password", + "disconnect.username_taken": "Username is taken", + + "message.chat.text": "%s: %s", + "message.connect.text": "[+] %s", + "message.disconnect.text": "[-] %s", + "message.edit_docs.text": "%s edited docs for %s", + "message.mark_deobf.text": "%s marked %s as deobfuscated", + "message.remove_mapping.text": "%s removed mappings for %s", + "message.rename.text": "%s renamed %s to %s", + + "status.disconnected": "Disconnected.", + "status.connected": "Connected.", + "status.connected_user_count": "Connected (%d users).", + "status.ready": "Ready.", + + "crash.title": "%s - Crash Report", + "crash.summary": "%s has crashed! =(", + "crash.export": "Export", + "crash.ignore": "Ignore", + "crash.exit": "Exit", + "crash.exit.warning": "If you choose exit, you will lose any unsaved work." +} diff --git a/enigma/src/main/resources/lang/fr_fr.json b/enigma/src/main/resources/lang/fr_fr.json new file mode 100644 index 0000000..d169b9a --- /dev/null +++ b/enigma/src/main/resources/lang/fr_fr.json @@ -0,0 +1,164 @@ +{ + "language": "Français", + + "mapping_format.enigma_file": "Fichier Enigma", + "mapping_format.enigma_directory": "Répertoire Enigma", + "mapping_format.enigma_zip": "ZIP Enigma", + "mapping_format.tiny_v2": "Tiny v2", + "mapping_format.tiny_file": "Fichier Tiny", + "mapping_format.srg_file": "Fichier SRG", + "mapping_format.proguard": "Proguard", + "type.methods": "Méthodes", + "type.fields": "Champs", + "type.parameters": "Paramètres", + "type.classes": "Classes", + + "menu.file": "Fichier", + "menu.file.jar.open": "Ouvrir le jar...", + "menu.file.jar.close": "Fermer le jar", + "menu.file.mappings.open": "Ouvrir les mappings...", + "menu.file.mappings.save": "Enregistrer les mappings", + "menu.file.mappings.save_as": "Enregistrer les mappings sous...", + "menu.file.mappings.close": "Fermer les mappings", + "menu.file.mappings.drop": "Supprimer les mappings invalides", + "menu.file.export.source": "Exporter la source...", + "menu.file.export.jar": "Exporter le jar...", + "menu.file.stats": "Statistiques des mappings...", + "menu.file.stats.title": "Choisir les membres inclus", + "menu.file.stats.generate": "Générer les statistiques", + "menu.file.exit": "Quitter", + "menu.decompiler": "Décompilateur", + "menu.view": "Affichage", + "menu.view.themes": "Thèmes", + "menu.view.themes.default": "Par défaut", + "menu.view.themes.darcula": "Darcula", + "menu.view.themes.system": "Système", + "menu.view.themes.none": "Aucun (JVM par défaut)", + "menu.view.languages": "Langues", + "menu.view.scale": "Échelle", + "menu.view.scale.custom": "Personnalisée...", + "menu.view.scale.custom.title": "Échelle personnalisée", + "menu.view.change.title": "Modifications", + "menu.view.change.summary": "Les modifications seront appliquées lors du prochain redémarrage.", + "menu.view.change.ok": "Ok", + "menu.view.search": "Rechercher", + "menu.collab": "Collab", + "menu.collab.connect": "Se connecter à un serveur", + "menu.collab.connect.error": "Erreur lors de la connexion au serveur", + "menu.collab.disconnect": "Se déconnecter", + "menu.collab.server.start": "Démarrer le serveur", + "menu.collab.server.start.error": "Erreur lors du démarrage du serveur", + "menu.collab.server.stop": "Arrêter le serveur", + "menu.help": "Aide", + "menu.help.about": "À propos", + "menu.help.about.title": "%s - À propos", + "menu.help.about.ok": "Ok", + "menu.help.github": "Page Github", + + "popup_menu.rename": "Renommer", + "popup_menu.javadoc": "Éditer le Javadoc", + "popup_menu.inheritance": "Afficher l'héritage", + "popup_menu.implementations": "Afficher les implémentations", + "popup_menu.calls": "Afficher les appels (tous)", + "popup_menu.calls.specific": "Afficher les appels (spécifiques)", + "popup_menu.declaration": "Aller à la déclaration", + "popup_menu.back": "Annuler", + "popup_menu.forward": "Restaurer", + "popup_menu.mark_deobfuscated": "Marquer comme déobfusqué", + "popup_menu.reset_obfuscated": "Réinitialiser à obfusqué", + "popup_menu.zoom.in": "Zoomer", + "popup_menu.zoom.out": "Dézoomer", + "popup_menu.zoom.reset": "Réinitialiser le zoom", + + "info_panel.classes.obfuscated": "Classes obfusquées", + "info_panel.classes.deobfuscated": "Classes déobfusquées", + "info_panel.identifier": "Informations sur l'identifiant", + "info_panel.identifier.none": "Aucun identifiant sélectionné", + "info_panel.identifier.variable": "Variable", + "info_panel.identifier.field": "Champ", + "info_panel.identifier.method": "Méthode", + "info_panel.identifier.constructor": "Constructeur", + "info_panel.identifier.class": "Classe", + "info_panel.identifier.type_descriptor": "Descripteur de type", + "info_panel.identifier.method_descriptor": "Descripteur de méthode", + "info_panel.identifier.modifier": "Modificateur", + "info_panel.identifier.index": "Index", + "info_panel.editor.class.decompiling": "(décompilation...)", + "info_panel.editor.class.not_found": "Impossible de trouver la classe :", + "info_panel.tree.inheritance": "Héritage", + "info_panel.tree.implementations": "Implémentations", + "info_panel.tree.calls": "Graphique des appels", + + "log_panel.messages": "Messages", + "log_panel.users": "Utilisateurs", + + "progress.operation": "%s - Opération en cours", + "progress.jar.indexing": "Indexation du jar", + "progress.jar.indexing.entries": "Entrées...", + "progress.jar.indexing.references": "Références des entrées...", + "progress.jar.indexing.methods": "Mise en place des méthodes...", + "progress.jar.indexing.process": "Traitement...", + "progress.jar.writing": "Écriture du jar...", + "progress.sources.writing": "Écriture des sources...", + "progress.classes.deobfuscating": "Déobfuscation des classes...", + "progress.classes.decompiling": "Décompilation des classes...", + "progress.mappings.enigma_file.loading": "Chargement du fichier de mappings", + "progress.mappings.enigma_file.done": "Terminé !", + "progress.mappings.enigma_file.writing": "Écriture des classes", + "progress.mappings.enigma_directory.loading": "Chargement des fichiers de mappings", + "progress.mappings.enigma_directory.writing": "Écriture des classes", + "progress.mappings.tiny_file.loading": "Chargement du fichier de mappings", + "progress.mappings.tiny_v2.loading": "Chargement du fichier de mappings", + "progress.mappings.srg_file.generating": "Génération des mappings", + "progress.mappings.srg_file.writing": "Écriture des mappings", + "progress.stats": "Génération des statistiques", + "progress.stats.data": "Génération des données", + + "javadocs.edit": "Éditer les Javadocs", + "javadocs.instruction": "Éditer les Javadocs ici.", + "javadocs.cancel": "Annuler", + "javadocs.save": "Enregistrer", + + "prompt.close.title": "Enregistrer les modifications ?", + "prompt.close.summary": "Vos mappings n'ont pas encore été enregistrés. Souhaitez-vous enregistrer ?", + "prompt.close.save": "Enregistrer et fermer", + "prompt.close.discard": "Annuler les modifications", + "prompt.close.cancel": "Annuler", + "prompt.open": "Ouvrir", + "prompt.cancel": "Annuler", + "prompt.connect.title": "Se connecter à un serveur", + "prompt.connect.username": "Nom d'utilisateur", + "prompt.connect.ip": "IP", + "prompt.port": "Port", + "prompt.port.nan": "Le port n'est pas un nombre", + "prompt.port.invalid": "Le port est hors de portée. Il doit être compris entre 0 et 65535.", + "prompt.password": "Mot de passe", + "prompt.password.too_long": "Le mot de passe est trop long. Il ne doit pas dépasser 255 caractères.", + "prompt.create_server.title": "Créer un serveur", + + "disconnect.disconnected": "Déconnecté", + "disconnect.server_closed": "Serveur fermé", + "disconnect.wrong_jar": "Les sommes de contrôle du jar ne correspondent pas (vous avez le mauvais jar) !", + "disconnect.wrong_password": "Mot de passe incorrect", + "disconnect.username_taken": "Le nom d'utilisateur est déjà pris", + + "message.chat.text": "%s : %s", + "message.connect.text": "[+] %s", + "message.disconnect.text": "[-] %s", + "message.edit_docs.text": "%s a édité les javadocs de %s", + "message.mark_deobf.text": "%s a marqué %s comme déobfusqué", + "message.remove_mapping.text": "%s a supprimé les mappings de %s", + "message.rename.text": "%s a renommé %s en %s", + + "status.disconnected": "Déconnecté.", + "status.connected": "Connecté.", + "status.connected_user_count": "Connecté (%d utilisateurs).", + "status.ready": "Prêt.", + + "crash.title": "%s - Rapport de plantage", + "crash.summary": "%s a planté ! =(", + "crash.export": "Exporter", + "crash.ignore": "Ignorer", + "crash.exit": "Quitter", + "crash.exit.warning": "Si vous choisissez Quitter, vous perdrez tout travail non sauvegardé." +} diff --git a/enigma/src/main/resources/lang/zh_cn.json b/enigma/src/main/resources/lang/zh_cn.json new file mode 100644 index 0000000..f3f503a --- /dev/null +++ b/enigma/src/main/resources/lang/zh_cn.json @@ -0,0 +1,118 @@ +{ + "language": "Chinese", + + "mapping_format.enigma_file": "Enigma 文件", + "mapping_format.enigma_directory": "Enigma 目录", + "mapping_format.enigma_zip": "Enigma ZIP", + "mapping_format.tiny_v2": "Tiny v2", + "mapping_format.tiny_file": "Tiny File", + "mapping_format.srg_file": "SRG File", + "mapping_format.proguard": "Proguard", + "type.methods": "方法", + "type.fields": "字段", + "type.parameters": "参数", + "type.classes": "类", + + "menu.file": "文件", + "menu.file.jar.open": "打开 Jar...", + "menu.file.jar.close": "关闭 Jar", + "menu.file.mappings.open": "打开映射...", + "menu.file.mappings.save": "保存映射", + "menu.file.mappings.save_as": "将映射另存为...", + "menu.file.mappings.close": "关闭映射", + "menu.file.mappings.drop": "删除无效映射", + "menu.file.export.source": "导出源码...", + "menu.file.export.jar": "导出Jar...", + "menu.file.stats": "映射统计范围...", + "menu.file.stats.title": "选择包括的成员", + "menu.file.stats.generate": "生成统计范围", + "menu.file.exit": "退出", + "menu.decompiler": "反编译", + "menu.view": "查看", + "menu.view.themes": "主题", + "menu.view.themes.default": "Default", + "menu.view.themes.darcula": "Darcula", + "menu.view.themes.system": "System", + "menu.view.themes.none": "None (JVM Default)", + "menu.view.languages": "语言", + "menu.view.languages.title": "更改语言", + "menu.view.languages.summary": "新语言将在下次重新启动后应用.", + "menu.view.languages.ok": "确定", + "menu.view.search": "搜索", + "menu.help": "帮助", + "menu.help.about": "关于", + "menu.help.about.title": "%s - 关于", + "menu.help.about.ok": "确定", + "menu.help.github": "GitHub 页面", + + "popup_menu.rename": "改名", + "popup_menu.javadoc": "编辑注释", + "popup_menu.inheritance": "显示继承", + "popup_menu.implementations": "显示实现", + "popup_menu.calls": "显示 Calls", + "popup_menu.calls.specific": "显示 Calls (具体)", + "popup_menu.declaration": "Go to Declaration", + "popup_menu.back": "Go back", + "popup_menu.forward": "Go forward", + "popup_menu.mark_deobfuscated": "标记为反混淆", + "popup_menu.reset_obfuscated": "重置混淆", + + "info_panel.classes.obfuscated": "混淆类", + "info_panel.classes.deobfuscated": "反混淆类", + "info_panel.identifier": "标识符信息", + "info_panel.identifier.none": "未选择标识符", + "info_panel.identifier.variable": "变量", + "info_panel.identifier.field": "字段", + "info_panel.identifier.method": "方法", + "info_panel.identifier.constructor": "构造器", + "info_panel.identifier.class": "类", + "info_panel.identifier.type_descriptor": "类型描述符", + "info_panel.identifier.method_descriptor": "方法描述符", + "info_panel.identifier.modifier": "修饰语", + "info_panel.identifier.index": "索引", + "info_panel.editor.class.decompiling": "(反编译中...)", + "info_panel.editor.class.not_found": "找不到类:", + "info_panel.tree.inheritance": "继承", + "info_panel.tree.implementations": "实现", + "info_panel.tree.calls": "调用图", + + "progress.operation": "%s - 进行中", + "progress.jar.indexing": "索引jar", + "progress.jar.indexing.entries": "条目...", + "progress.jar.indexing.references": "条目引用...", + "progress.jar.indexing.methods": "桥接方法...", + "progress.jar.indexing.process": "处理中...", + "progress.jar.writing": "写出jar中...", + "progress.sources.writing": "写出源码中...", + "progress.classes.deobfuscating": "反混淆类中...", + "progress.classes.decompiling": "反编译类中...", + "progress.mappings.enigma_file.loading": "加载映射文件", + "progress.mappings.enigma_file.done": "完成!", + "progress.mappings.enigma_file.writing": "写出类", + "progress.mappings.enigma_directory.loading": "加载映射文件", + "progress.mappings.enigma_directory.writing": "写出类", + "progress.mappings.tiny_file.loading": "加载映射文件", + "progress.mappings.tiny_v2.loading": "加载映射文件", + "progress.mappings.srg_file.generating": "生成映射", + "progress.mappings.srg_file.writing": "写出映射", + "progress.stats": "生成统计范围", + "progress.stats.data": "生成数据", + + "javadocs.edit": "编辑注释", + "javadocs.instruction": "在此处编辑编辑注释.", + "javadocs.cancel": "取消", + "javadocs.save": "保存", + + "prompt.close.title": "保存更改?", + "prompt.close.summary": "您的映射尚未保存。你想保存吗?", + "prompt.close.save": "保存并关闭", + "prompt.close.discard": "放弃更改", + "prompt.close.cancel": "取消", + + "crash.title": "%s - 崩溃报告", + "crash.summary": "%s 已经崩溃! =(", + "crash.export": "输出", + "crash.ignore": "忽略", + "crash.exit": "退出", + "crash.exit.warning": "如果选择退出,将丢失所有未保存的工作." +} diff --git a/enigma/src/main/resources/profile.json b/enigma/src/main/resources/profile.json new file mode 100644 index 0000000..e1af4cd --- /dev/null +++ b/enigma/src/main/resources/profile.json @@ -0,0 +1,20 @@ +{ + "services": { + "jar_indexer": [ + { + "id": "enigma:enum_initializer_indexer" + }, + { + "id": "enigma:specialized_bridge_method_indexer" + } + ], + "name_proposal": [ + { + "id": "enigma:enum_name_proposer" + }, + { + "id": "enigma:specialized_method_name_proposer" + } + ] + } +} \ No newline at end of file diff --git a/enigma/src/test/java/cuchaz/enigma/PackageVisibilityIndexTest.java b/enigma/src/test/java/cuchaz/enigma/PackageVisibilityIndexTest.java new file mode 100644 index 0000000..1dc9748 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/PackageVisibilityIndexTest.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import cuchaz.enigma.analysis.ClassCache; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.analysis.index.PackageVisibilityIndex; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import org.junit.Test; + +import java.nio.file.Paths; + +import static cuchaz.enigma.TestEntryFactory.newClass; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; + +public class PackageVisibilityIndexTest { + + private static final ClassEntry KEEP = newClass("cuchaz/enigma/inputs/Keep"); + private static final ClassEntry BASE = newClass("a"); + private static final ClassEntry SAME_PACKAGE_CHILD = newClass("b"); + private static final ClassEntry SAME_PACKAGE_CHILD_INNER = newClass("b$a"); + private static final ClassEntry OTHER_PACKAGE_CHILD = newClass("c"); + private static final ClassEntry OTHER_PACKAGE_CHILD_INNER = newClass("c$a"); + private final JarIndex jarIndex; + + public PackageVisibilityIndexTest() throws Exception { + ClassCache classCache = ClassCache.of(Paths.get("build/test-obf/packageAccess.jar")); + jarIndex = classCache.index(ProgressListener.none()); + } + + @Test + public void test() { + PackageVisibilityIndex visibilityIndex = jarIndex.getPackageVisibilityIndex(); + assertThat(visibilityIndex.getPartition(BASE), containsInAnyOrder(BASE, SAME_PACKAGE_CHILD, SAME_PACKAGE_CHILD_INNER)); + System.out.println(visibilityIndex.getPartitions()); + assertThat(visibilityIndex.getPartitions(), containsInAnyOrder( + containsInAnyOrder(BASE, SAME_PACKAGE_CHILD, SAME_PACKAGE_CHILD_INNER), + containsInAnyOrder(OTHER_PACKAGE_CHILD, OTHER_PACKAGE_CHILD_INNER), + contains(KEEP) + )); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/TestDeobfed.java b/enigma/src/test/java/cuchaz/enigma/TestDeobfed.java new file mode 100644 index 0000000..494d959 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TestDeobfed.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import cuchaz.enigma.analysis.ClassCache; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.source.Decompiler; +import cuchaz.enigma.source.Decompilers; +import cuchaz.enigma.source.SourceSettings; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static cuchaz.enigma.TestEntryFactory.newClass; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; + +public class TestDeobfed { + private static Enigma enigma; + private static ClassCache classCache; + private static JarIndex index; + + @BeforeClass + public static void beforeClass() throws Exception { + enigma = Enigma.create(); + + Path obf = Paths.get("build/test-obf/translation.jar"); + Path deobf = Paths.get("build/test-deobf/translation.jar"); + Files.createDirectories(deobf.getParent()); + EnigmaProject project = enigma.openJar(obf, ProgressListener.none()); + project.exportRemappedJar(ProgressListener.none()).write(deobf, ProgressListener.none()); + + classCache = ClassCache.of(deobf); + index = classCache.index(ProgressListener.none()); + } + + @Test + public void obfEntries() { + assertThat(index.getEntryIndex().getClasses(), containsInAnyOrder( + newClass("cuchaz/enigma/inputs/Keep"), + newClass("a"), + newClass("b"), + newClass("c"), + newClass("d"), + newClass("d$1"), + newClass("e"), + newClass("f"), + newClass("g"), + newClass("g$a"), + newClass("g$a$a"), + newClass("g$b"), + newClass("g$b$a"), + newClass("h"), + newClass("h$a"), + newClass("h$a$a"), + newClass("h$b"), + newClass("h$b$a"), + newClass("h$b$a$a"), + newClass("h$b$a$b"), + newClass("i"), + newClass("i$a"), + newClass("i$b") + )); + } + + @Test + public void decompile() { + EnigmaProject project = new EnigmaProject(enigma, classCache, index, new byte[20]); + Decompiler decompiler = Decompilers.PROCYON.create(project.getClassCache(), new SourceSettings(false, false)); + + decompiler.getSource("a"); + decompiler.getSource("b"); + decompiler.getSource("c"); + decompiler.getSource("d"); + decompiler.getSource("d$1"); + decompiler.getSource("e"); + decompiler.getSource("f"); + decompiler.getSource("g"); + decompiler.getSource("g$a"); + decompiler.getSource("g$a$a"); + decompiler.getSource("g$b"); + decompiler.getSource("g$b$a"); + decompiler.getSource("h"); + decompiler.getSource("h$a"); + decompiler.getSource("h$a$a"); + decompiler.getSource("h$b"); + decompiler.getSource("h$b$a"); + decompiler.getSource("h$b$a$a"); + decompiler.getSource("h$b$a$b"); + decompiler.getSource("i"); + decompiler.getSource("i$a"); + decompiler.getSource("i$b"); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/TestDeobfuscator.java b/enigma/src/test/java/cuchaz/enigma/TestDeobfuscator.java new file mode 100644 index 0000000..6619d26 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TestDeobfuscator.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import cuchaz.enigma.source.Decompiler; +import cuchaz.enigma.source.Decompilers; +import cuchaz.enigma.source.SourceSettings; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Paths; + +public class TestDeobfuscator { + private EnigmaProject openProject() throws IOException { + Enigma enigma = Enigma.create(); + return enigma.openJar(Paths.get("build/test-obf/loneClass.jar"), ProgressListener.none()); + } + + @Test + public void loadJar() + throws Exception { + openProject(); + } + + @Test + public void decompileClass() throws Exception { + EnigmaProject project = openProject(); + Decompiler decompiler = Decompilers.PROCYON.create(project.getClassCache(), new SourceSettings(false, false)); + + decompiler.getSource("a").asString(); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/TestEntryFactory.java b/enigma/src/test/java/cuchaz/enigma/TestEntryFactory.java new file mode 100644 index 0000000..9e1425a --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TestEntryFactory.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.translation.representation.*; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +public class TestEntryFactory { + + public static ClassEntry newClass(String name) { + return new ClassEntry(name); + } + + public static FieldEntry newField(String className, String fieldName, String fieldType) { + return newField(newClass(className), fieldName, fieldType); + } + + public static FieldEntry newField(ClassEntry classEntry, String fieldName, String fieldType) { + return new FieldEntry(classEntry, fieldName, new TypeDescriptor(fieldType)); + } + + public static MethodEntry newMethod(String className, String methodName, String methodSignature) { + return newMethod(newClass(className), methodName, methodSignature); + } + + public static MethodEntry newMethod(ClassEntry classEntry, String methodName, String methodSignature) { + return new MethodEntry(classEntry, methodName, new MethodDescriptor(methodSignature)); + } + + public static EntryReference newFieldReferenceByMethod(FieldEntry fieldEntry, String callerClassName, String callerName, String callerSignature) { + return new EntryReference<>(fieldEntry, "", newMethod(callerClassName, callerName, callerSignature)); + } + + public static EntryReference newBehaviorReferenceByMethod(MethodEntry methodEntry, String callerClassName, String callerName, String callerSignature) { + return new EntryReference<>(methodEntry, "", newMethod(callerClassName, callerName, callerSignature)); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/TestInnerClasses.java b/enigma/src/test/java/cuchaz/enigma/TestInnerClasses.java new file mode 100644 index 0000000..85c72f8 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TestInnerClasses.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import cuchaz.enigma.analysis.ClassCache; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.source.Decompiler; +import cuchaz.enigma.source.Decompilers; +import cuchaz.enigma.source.SourceSettings; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import org.junit.Test; + +import java.nio.file.Paths; + +import static cuchaz.enigma.TestEntryFactory.newClass; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class TestInnerClasses { + + private static final ClassEntry SimpleOuter = newClass("d"); + private static final ClassEntry SimpleInner = newClass("d$a"); + private static final ClassEntry ConstructorArgsOuter = newClass("c"); + private static final ClassEntry ConstructorArgsInner = newClass("c$a"); + private static final ClassEntry ClassTreeRoot = newClass("f"); + private static final ClassEntry ClassTreeLevel1 = newClass("f$a"); + private static final ClassEntry ClassTreeLevel2 = newClass("f$a$a"); + private static final ClassEntry ClassTreeLevel3 = newClass("f$a$a$a"); + private final JarIndex index; + private final Decompiler decompiler; + + public TestInnerClasses() throws Exception { + ClassCache classCache = ClassCache.of(Paths.get("build/test-obf/innerClasses.jar")); + index = classCache.index(ProgressListener.none()); + decompiler = Decompilers.PROCYON.create(classCache, new SourceSettings(false, false)); + } + + @Test + public void simple() { + decompile(SimpleOuter); + } + + @Test + public void constructorArgs() { + decompile(ConstructorArgsOuter); + } + + @Test + public void classTree() { + + // root level + assertThat(index.getEntryIndex().hasClass(ClassTreeRoot), is(true)); + + // level 1 + ClassEntry fullClassEntry = new ClassEntry(ClassTreeRoot.getName() + + "$" + ClassTreeLevel1.getSimpleName()); + assertThat(index.getEntryIndex().hasClass(fullClassEntry), is(true)); + + // level 2 + fullClassEntry = new ClassEntry(ClassTreeRoot.getName() + + "$" + ClassTreeLevel1.getSimpleName() + + "$" + ClassTreeLevel2.getSimpleName()); + assertThat(index.getEntryIndex().hasClass(fullClassEntry), is(true)); + + // level 3 + fullClassEntry = new ClassEntry(ClassTreeRoot.getName() + + "$" + ClassTreeLevel1.getSimpleName() + + "$" + ClassTreeLevel2.getSimpleName() + + "$" + ClassTreeLevel3.getSimpleName()); + assertThat(index.getEntryIndex().hasClass(fullClassEntry), is(true)); + } + + private void decompile(ClassEntry classEntry) { + decompiler.getSource(classEntry.getName()); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/TestJarIndexConstructorReferences.java b/enigma/src/test/java/cuchaz/enigma/TestJarIndexConstructorReferences.java new file mode 100644 index 0000000..48975c8 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TestJarIndexConstructorReferences.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import cuchaz.enigma.analysis.ClassCache; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.MethodDefEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import org.junit.Test; + +import java.nio.file.Paths; +import java.util.Collection; + +import static cuchaz.enigma.TestEntryFactory.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class TestJarIndexConstructorReferences { + + private JarIndex index; + + private ClassEntry baseClass = newClass("a"); + private ClassEntry subClass = newClass("d"); + private ClassEntry subsubClass = newClass("e"); + private ClassEntry defaultClass = newClass("c"); + private ClassEntry callerClass = newClass("b"); + + public TestJarIndexConstructorReferences() throws Exception { + ClassCache classCache = ClassCache.of(Paths.get("build/test-obf/constructors.jar")); + index = classCache.index(ProgressListener.none()); + } + + @Test + public void obfEntries() { + assertThat(index.getEntryIndex().getClasses(), containsInAnyOrder(newClass("cuchaz/enigma/inputs/Keep"), baseClass, + subClass, subsubClass, defaultClass, callerClass)); + } + + @Test + @SuppressWarnings("unchecked") + public void baseDefault() { + MethodEntry source = newMethod(baseClass, "", "()V"); + Collection> references = index.getReferenceIndex().getReferencesToMethod(source); + assertThat(references, containsInAnyOrder( + newBehaviorReferenceByMethod(source, callerClass.getName(), "a", "()V"), + newBehaviorReferenceByMethod(source, subClass.getName(), "", "()V"), + newBehaviorReferenceByMethod(source, subClass.getName(), "", "(III)V") + )); + } + + @Test + @SuppressWarnings("unchecked") + public void baseInt() { + MethodEntry source = newMethod(baseClass, "", "(I)V"); + assertThat(index.getReferenceIndex().getReferencesToMethod(source), containsInAnyOrder( + newBehaviorReferenceByMethod(source, callerClass.getName(), "b", "()V") + )); + } + + @Test + @SuppressWarnings("unchecked") + public void subDefault() { + MethodEntry source = newMethod(subClass, "", "()V"); + assertThat(index.getReferenceIndex().getReferencesToMethod(source), containsInAnyOrder( + newBehaviorReferenceByMethod(source, callerClass.getName(), "c", "()V"), + newBehaviorReferenceByMethod(source, subClass.getName(), "", "(I)V") + )); + } + + @Test + @SuppressWarnings("unchecked") + public void subInt() { + MethodEntry source = newMethod(subClass, "", "(I)V"); + assertThat(index.getReferenceIndex().getReferencesToMethod(source), containsInAnyOrder( + newBehaviorReferenceByMethod(source, callerClass.getName(), "d", "()V"), + newBehaviorReferenceByMethod(source, subClass.getName(), "", "(II)V"), + newBehaviorReferenceByMethod(source, subsubClass.getName(), "", "(I)V") + )); + } + + @Test + @SuppressWarnings("unchecked") + public void subIntInt() { + MethodEntry source = newMethod(subClass, "", "(II)V"); + assertThat(index.getReferenceIndex().getReferencesToMethod(source), containsInAnyOrder( + newBehaviorReferenceByMethod(source, callerClass.getName(), "e", "()V") + )); + } + + @Test + public void subIntIntInt() { + MethodEntry source = newMethod(subClass, "", "(III)V"); + assertThat(index.getReferenceIndex().getReferencesToMethod(source), is(empty())); + } + + @Test + @SuppressWarnings("unchecked") + public void subsubInt() { + MethodEntry source = newMethod(subsubClass, "", "(I)V"); + assertThat(index.getReferenceIndex().getReferencesToMethod(source), containsInAnyOrder( + newBehaviorReferenceByMethod(source, callerClass.getName(), "f", "()V") + )); + } + + @Test + @SuppressWarnings("unchecked") + public void defaultConstructable() { + MethodEntry source = newMethod(defaultClass, "", "()V"); + assertThat(index.getReferenceIndex().getReferencesToMethod(source), containsInAnyOrder( + newBehaviorReferenceByMethod(source, callerClass.getName(), "g", "()V") + )); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/TestJarIndexInheritanceTree.java b/enigma/src/test/java/cuchaz/enigma/TestJarIndexInheritanceTree.java new file mode 100644 index 0000000..76e379c --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TestJarIndexInheritanceTree.java @@ -0,0 +1,227 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import cuchaz.enigma.analysis.ClassCache; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.analysis.index.EntryIndex; +import cuchaz.enigma.analysis.index.InheritanceIndex; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.IndexEntryResolver; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodDefEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import org.junit.Test; +import org.objectweb.asm.Opcodes; + +import java.nio.file.Paths; +import java.util.Collection; + +import static cuchaz.enigma.TestEntryFactory.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class TestJarIndexInheritanceTree { + + private JarIndex index; + + private ClassEntry baseClass = newClass("a"); + private ClassEntry subClassA = newClass("b"); + private ClassEntry subClassAA = newClass("d"); + private ClassEntry subClassB = newClass("c"); + private FieldEntry nameField = newField(baseClass, "a", "Ljava/lang/String;"); + private FieldEntry numThingsField = newField(subClassB, "a", "I"); + + public TestJarIndexInheritanceTree() + throws Exception { + ClassCache classCache = ClassCache.of(Paths.get("build/test-obf/inheritanceTree.jar")); + index = classCache.index(ProgressListener.none()); + } + + @Test + public void obfEntries() { + assertThat(index.getEntryIndex().getClasses(), containsInAnyOrder( + newClass("cuchaz/enigma/inputs/Keep"), baseClass, subClassA, subClassAA, subClassB + )); + } + + @Test + public void translationIndex() { + + InheritanceIndex index = this.index.getInheritanceIndex(); + + // base class + assertThat(index.getParents(baseClass), is(empty())); + assertThat(index.getAncestors(baseClass), is(empty())); + assertThat(index.getChildren(baseClass), containsInAnyOrder(subClassA, subClassB + )); + + // subclass a + assertThat(index.getParents(subClassA), contains(baseClass)); + assertThat(index.getAncestors(subClassA), containsInAnyOrder(baseClass)); + assertThat(index.getChildren(subClassA), contains(subClassAA)); + + // subclass aa + assertThat(index.getParents(subClassAA), contains(subClassA)); + assertThat(index.getAncestors(subClassAA), containsInAnyOrder(subClassA, baseClass)); + assertThat(index.getChildren(subClassAA), is(empty())); + + // subclass b + assertThat(index.getParents(subClassB), contains(baseClass)); + assertThat(index.getAncestors(subClassB), containsInAnyOrder(baseClass)); + assertThat(index.getChildren(subClassB), is(empty())); + } + + @Test + public void access() { + assertThat(index.getEntryIndex().getFieldAccess(nameField), is(new AccessFlags(Opcodes.ACC_PRIVATE))); + assertThat(index.getEntryIndex().getFieldAccess(numThingsField), is(new AccessFlags(Opcodes.ACC_PRIVATE))); + } + + @Test + public void relatedMethodImplementations() { + + Collection entries; + + EntryResolver resolver = new IndexEntryResolver(index); + // getName() + entries = resolver.resolveEquivalentMethods(newMethod(baseClass, "a", "()Ljava/lang/String;")); + assertThat(entries, containsInAnyOrder( + newMethod(baseClass, "a", "()Ljava/lang/String;"), + newMethod(subClassAA, "a", "()Ljava/lang/String;") + )); + entries = resolver.resolveEquivalentMethods(newMethod(subClassAA, "a", "()Ljava/lang/String;")); + assertThat(entries, containsInAnyOrder( + newMethod(baseClass, "a", "()Ljava/lang/String;"), + newMethod(subClassAA, "a", "()Ljava/lang/String;") + )); + + // doBaseThings() + entries = resolver.resolveEquivalentMethods(newMethod(baseClass, "a", "()V")); + assertThat(entries, containsInAnyOrder( + newMethod(baseClass, "a", "()V"), + newMethod(subClassAA, "a", "()V"), + newMethod(subClassB, "a", "()V") + )); + entries = resolver.resolveEquivalentMethods(newMethod(subClassAA, "a", "()V")); + assertThat(entries, containsInAnyOrder( + newMethod(baseClass, "a", "()V"), + newMethod(subClassAA, "a", "()V"), + newMethod(subClassB, "a", "()V") + )); + entries = resolver.resolveEquivalentMethods(newMethod(subClassB, "a", "()V")); + assertThat(entries, containsInAnyOrder( + newMethod(baseClass, "a", "()V"), + newMethod(subClassAA, "a", "()V"), + newMethod(subClassB, "a", "()V") + )); + + // doBThings + entries = resolver.resolveEquivalentMethods(newMethod(subClassB, "b", "()V")); + assertThat(entries, containsInAnyOrder(newMethod(subClassB, "b", "()V"))); + } + + @Test + @SuppressWarnings("unchecked") + public void fieldReferences() { + Collection> references; + + // name + references = index.getReferenceIndex().getReferencesToField(nameField); + assertThat(references, containsInAnyOrder( + newFieldReferenceByMethod(nameField, baseClass.getName(), "", "(Ljava/lang/String;)V"), + newFieldReferenceByMethod(nameField, baseClass.getName(), "a", "()Ljava/lang/String;") + )); + + // numThings + references = index.getReferenceIndex().getReferencesToField(numThingsField); + assertThat(references, containsInAnyOrder( + newFieldReferenceByMethod(numThingsField, subClassB.getName(), "", "()V"), + newFieldReferenceByMethod(numThingsField, subClassB.getName(), "b", "()V") + )); + } + + @Test + @SuppressWarnings("unchecked") + public void behaviorReferences() { + + MethodEntry source; + Collection> references; + + // baseClass constructor + source = newMethod(baseClass, "", "(Ljava/lang/String;)V"); + references = index.getReferenceIndex().getReferencesToMethod(source); + assertThat(references, containsInAnyOrder( + newBehaviorReferenceByMethod(source, subClassA.getName(), "", "(Ljava/lang/String;)V"), + newBehaviorReferenceByMethod(source, subClassB.getName(), "", "()V") + )); + + // subClassA constructor + source = newMethod(subClassA, "", "(Ljava/lang/String;)V"); + references = index.getReferenceIndex().getReferencesToMethod(source); + assertThat(references, containsInAnyOrder( + newBehaviorReferenceByMethod(source, subClassAA.getName(), "", "()V") + )); + + // baseClass.getName() + source = newMethod(baseClass, "a", "()Ljava/lang/String;"); + references = index.getReferenceIndex().getReferencesToMethod(source); + assertThat(references, containsInAnyOrder( + newBehaviorReferenceByMethod(source, subClassAA.getName(), "a", "()Ljava/lang/String;"), + newBehaviorReferenceByMethod(source, subClassB.getName(), "a", "()V") + )); + + // subclassAA.getName() + source = newMethod(subClassAA, "a", "()Ljava/lang/String;"); + references = index.getReferenceIndex().getReferencesToMethod(source); + assertThat(references, containsInAnyOrder( + newBehaviorReferenceByMethod(source, subClassAA.getName(), "a", "()V") + )); + } + + @Test + public void containsEntries() { + EntryIndex entryIndex = index.getEntryIndex(); + // classes + assertThat(entryIndex.hasClass(baseClass), is(true)); + assertThat(entryIndex.hasClass(subClassA), is(true)); + assertThat(entryIndex.hasClass(subClassAA), is(true)); + assertThat(entryIndex.hasClass(subClassB), is(true)); + + // fields + assertThat(entryIndex.hasField(nameField), is(true)); + assertThat(entryIndex.hasField(numThingsField), is(true)); + + // methods + // getName() + assertThat(entryIndex.hasMethod(newMethod(baseClass, "a", "()Ljava/lang/String;")), is(true)); + assertThat(entryIndex.hasMethod(newMethod(subClassA, "a", "()Ljava/lang/String;")), is(false)); + assertThat(entryIndex.hasMethod(newMethod(subClassAA, "a", "()Ljava/lang/String;")), is(true)); + assertThat(entryIndex.hasMethod(newMethod(subClassB, "a", "()Ljava/lang/String;")), is(false)); + + // doBaseThings() + assertThat(entryIndex.hasMethod(newMethod(baseClass, "a", "()V")), is(true)); + assertThat(entryIndex.hasMethod(newMethod(subClassA, "a", "()V")), is(false)); + assertThat(entryIndex.hasMethod(newMethod(subClassAA, "a", "()V")), is(true)); + assertThat(entryIndex.hasMethod(newMethod(subClassB, "a", "()V")), is(true)); + + // doBThings() + assertThat(entryIndex.hasMethod(newMethod(baseClass, "b", "()V")), is(false)); + assertThat(entryIndex.hasMethod(newMethod(subClassA, "b", "()V")), is(false)); + assertThat(entryIndex.hasMethod(newMethod(subClassAA, "b", "()V")), is(false)); + assertThat(entryIndex.hasMethod(newMethod(subClassB, "b", "()V")), is(true)); + + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/TestJarIndexLoneClass.java b/enigma/src/test/java/cuchaz/enigma/TestJarIndexLoneClass.java new file mode 100644 index 0000000..103c366 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TestJarIndexLoneClass.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import cuchaz.enigma.analysis.*; +import cuchaz.enigma.analysis.index.EntryIndex; +import cuchaz.enigma.analysis.index.InheritanceIndex; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.VoidTranslator; +import cuchaz.enigma.translation.representation.AccessFlags; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodDefEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import org.junit.Test; + +import java.nio.file.Paths; +import java.util.Collection; +import java.util.List; + +import static cuchaz.enigma.TestEntryFactory.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class TestJarIndexLoneClass { + + private JarIndex index; + + public TestJarIndexLoneClass() throws Exception { + ClassCache classCache = ClassCache.of(Paths.get("build/test-obf/loneClass.jar")); + index = classCache.index(ProgressListener.none()); + } + + @Test + public void obfEntries() { + assertThat(index.getEntryIndex().getClasses(), containsInAnyOrder( + newClass("cuchaz/enigma/inputs/Keep"), + newClass("a") + )); + } + + @Test + public void translationIndex() { + InheritanceIndex inheritanceIndex = index.getInheritanceIndex(); + assertThat(inheritanceIndex.getParents(new ClassEntry("a")), is(empty())); + assertThat(inheritanceIndex.getParents(new ClassEntry("cuchaz/enigma/inputs/Keep")), is(empty())); + assertThat(inheritanceIndex.getAncestors(new ClassEntry("a")), is(empty())); + assertThat(inheritanceIndex.getAncestors(new ClassEntry("cuchaz/enigma/inputs/Keep")), is(empty())); + assertThat(inheritanceIndex.getChildren(new ClassEntry("a")), is(empty())); + assertThat(inheritanceIndex.getChildren(new ClassEntry("cuchaz/enigma/inputs/Keep")), is(empty())); + } + + @Test + public void access() { + EntryIndex entryIndex = index.getEntryIndex(); + assertThat(entryIndex.getFieldAccess(newField("a", "a", "Ljava/lang/String;")), is(AccessFlags.PRIVATE)); + assertThat(entryIndex.getMethodAccess(newMethod("a", "a", "()Ljava/lang/String;")), is(AccessFlags.PUBLIC)); + assertThat(entryIndex.getFieldAccess(newField("a", "b", "Ljava/lang/String;")), is(nullValue())); + assertThat(entryIndex.getFieldAccess(newField("a", "a", "LFoo;")), is(nullValue())); + } + + @Test + public void classInheritance() { + IndexTreeBuilder treeBuilder = new IndexTreeBuilder(index); + ClassInheritanceTreeNode node = treeBuilder.buildClassInheritance(VoidTranslator.INSTANCE, newClass("a")); + assertThat(node, is(not(nullValue()))); + assertThat(node.getObfClassName(), is("a")); + assertThat(node.getChildCount(), is(0)); + } + + @Test + public void methodInheritance() { + IndexTreeBuilder treeBuilder = new IndexTreeBuilder(index); + MethodEntry source = newMethod("a", "a", "()Ljava/lang/String;"); + MethodInheritanceTreeNode node = treeBuilder.buildMethodInheritance(VoidTranslator.INSTANCE, source); + assertThat(node, is(not(nullValue()))); + assertThat(node.getMethodEntry(), is(source)); + assertThat(node.getChildCount(), is(0)); + } + + @Test + public void classImplementations() { + IndexTreeBuilder treeBuilder = new IndexTreeBuilder(index); + ClassImplementationsTreeNode node = treeBuilder.buildClassImplementations(VoidTranslator.INSTANCE, newClass("a")); + assertThat(node, is(nullValue())); + } + + @Test + public void methodImplementations() { + IndexTreeBuilder treeBuilder = new IndexTreeBuilder(index); + MethodEntry source = newMethod("a", "a", "()Ljava/lang/String;"); + + List nodes = treeBuilder.buildMethodImplementations(VoidTranslator.INSTANCE, source); + assertThat(nodes, hasSize(1)); + assertThat(nodes.get(0).getMethodEntry(), is(source)); + } + + @Test + public void relatedMethodImplementations() { + Collection entries = index.getEntryResolver().resolveEquivalentMethods(newMethod("a", "a", "()Ljava/lang/String;")); + assertThat(entries, containsInAnyOrder( + newMethod("a", "a", "()Ljava/lang/String;") + )); + } + + @Test + @SuppressWarnings("unchecked") + public void fieldReferences() { + FieldEntry source = newField("a", "a", "Ljava/lang/String;"); + Collection> references = index.getReferenceIndex().getReferencesToField(source); + assertThat(references, containsInAnyOrder( + newFieldReferenceByMethod(source, "a", "", "(Ljava/lang/String;)V"), + newFieldReferenceByMethod(source, "a", "a", "()Ljava/lang/String;") + )); + } + + @Test + public void behaviorReferences() { + assertThat(index.getReferenceIndex().getReferencesToMethod(newMethod("a", "a", "()Ljava/lang/String;")), is(empty())); + } + + @Test + public void interfaces() { + assertThat(index.getInheritanceIndex().getParents(new ClassEntry("a")), is(empty())); + } + + @Test + public void implementingClasses() { + assertThat(index.getInheritanceIndex().getChildren(new ClassEntry("a")), is(empty())); + } + + @Test + public void isInterface() { + assertThat(index.getInheritanceIndex().isParent(new ClassEntry("a")), is(false)); + } + + @Test + public void testContains() { + EntryIndex entryIndex = index.getEntryIndex(); + assertThat(entryIndex.hasClass(newClass("a")), is(true)); + assertThat(entryIndex.hasClass(newClass("b")), is(false)); + assertThat(entryIndex.hasField(newField("a", "a", "Ljava/lang/String;")), is(true)); + assertThat(entryIndex.hasField(newField("a", "b", "Ljava/lang/String;")), is(false)); + assertThat(entryIndex.hasField(newField("a", "a", "LFoo;")), is(false)); + assertThat(entryIndex.hasMethod(newMethod("a", "a", "()Ljava/lang/String;")), is(true)); + assertThat(entryIndex.hasMethod(newMethod("a", "b", "()Ljava/lang/String;")), is(false)); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/TestMethodDescriptor.java b/enigma/src/test/java/cuchaz/enigma/TestMethodDescriptor.java new file mode 100644 index 0000000..a73880d --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TestMethodDescriptor.java @@ -0,0 +1,247 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class TestMethodDescriptor { + + @Test + public void easiest() { + final MethodDescriptor sig = new MethodDescriptor("()V"); + assertThat(sig.getArgumentDescs(), is(empty())); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("V"))); + } + + @Test + public void primitives() { + { + final MethodDescriptor sig = new MethodDescriptor("(I)V"); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("I") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("V"))); + } + { + final MethodDescriptor sig = new MethodDescriptor("(I)I"); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("I") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("I"))); + } + { + final MethodDescriptor sig = new MethodDescriptor("(IBCJ)Z"); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("I"), + new TypeDescriptor("B"), + new TypeDescriptor("C"), + new TypeDescriptor("J") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("Z"))); + } + } + + @Test + public void classes() { + { + final MethodDescriptor sig = new MethodDescriptor("([LFoo;)V"); + assertThat(sig.getArgumentDescs().size(), is(1)); + assertThat(sig.getArgumentDescs().get(0), is(new TypeDescriptor("[LFoo;"))); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("V"))); + } + { + final MethodDescriptor sig = new MethodDescriptor("(LFoo;)LBar;"); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("LFoo;") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("LBar;"))); + } + { + final MethodDescriptor sig = new MethodDescriptor("(LFoo;LMoo;LZoo;)LBar;"); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("LFoo;"), + new TypeDescriptor("LMoo;"), + new TypeDescriptor("LZoo;") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("LBar;"))); + } + } + + @Test + public void arrays() { + { + final MethodDescriptor sig = new MethodDescriptor("([I)V"); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("[I") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("V"))); + } + { + final MethodDescriptor sig = new MethodDescriptor("([I)[J"); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("[I") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("[J"))); + } + { + final MethodDescriptor sig = new MethodDescriptor("([I[Z[F)[D"); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("[I"), + new TypeDescriptor("[Z"), + new TypeDescriptor("[F") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("[D"))); + } + } + + @Test + public void mixed() { + { + final MethodDescriptor sig = new MethodDescriptor("(I[JLFoo;)Z"); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("I"), + new TypeDescriptor("[J"), + new TypeDescriptor("LFoo;") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("Z"))); + } + { + final MethodDescriptor sig = new MethodDescriptor("(III)[LFoo;"); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("I"), + new TypeDescriptor("I"), + new TypeDescriptor("I") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("[LFoo;"))); + } + } + + @Test + public void replaceClasses() { + { + final MethodDescriptor oldSig = new MethodDescriptor("()V"); + final MethodDescriptor sig = oldSig.remap(s -> null); + assertThat(sig.getArgumentDescs(), is(empty())); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("V"))); + } + { + final MethodDescriptor oldSig = new MethodDescriptor("(IJLFoo;)V"); + final MethodDescriptor sig = oldSig.remap(s -> null); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("I"), + new TypeDescriptor("J"), + new TypeDescriptor("LFoo;") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("V"))); + } + { + final MethodDescriptor oldSig = new MethodDescriptor("(LFoo;LBar;)LMoo;"); + final MethodDescriptor sig = oldSig.remap(s -> { + if (s.equals("Foo")) { + return "Bar"; + } + return null; + }); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("LBar;"), + new TypeDescriptor("LBar;") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("LMoo;"))); + } + { + final MethodDescriptor oldSig = new MethodDescriptor("(LFoo;LBar;)LMoo;"); + final MethodDescriptor sig = oldSig.remap(s -> { + if (s.equals("Moo")) { + return "Cow"; + } + return null; + }); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("LFoo;"), + new TypeDescriptor("LBar;") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("LCow;"))); + } + } + + @Test + public void replaceArrayClasses() { + { + final MethodDescriptor oldSig = new MethodDescriptor("([LFoo;)[[[LBar;"); + final MethodDescriptor sig = oldSig.remap(s -> { + if (s.equals("Foo")) { + return "Food"; + } else if (s.equals("Bar")) { + return "Beer"; + } + return null; + }); + assertThat(sig.getArgumentDescs(), contains( + new TypeDescriptor("[LFood;") + )); + assertThat(sig.getReturnDesc(), is(new TypeDescriptor("[[[LBeer;"))); + } + } + + @Test + public void equals() { + + // base + assertThat(new MethodDescriptor("()V"), is(new MethodDescriptor("()V"))); + + // arguments + assertThat(new MethodDescriptor("(I)V"), is(new MethodDescriptor("(I)V"))); + assertThat(new MethodDescriptor("(ZIZ)V"), is(new MethodDescriptor("(ZIZ)V"))); + assertThat(new MethodDescriptor("(LFoo;)V"), is(new MethodDescriptor("(LFoo;)V"))); + assertThat(new MethodDescriptor("(LFoo;LBar;)V"), is(new MethodDescriptor("(LFoo;LBar;)V"))); + assertThat(new MethodDescriptor("([I)V"), is(new MethodDescriptor("([I)V"))); + assertThat(new MethodDescriptor("([[D[[[J)V"), is(new MethodDescriptor("([[D[[[J)V"))); + + assertThat(new MethodDescriptor("()V"), is(not(new MethodDescriptor("(I)V")))); + assertThat(new MethodDescriptor("(I)V"), is(not(new MethodDescriptor("()V")))); + assertThat(new MethodDescriptor("(IJ)V"), is(not(new MethodDescriptor("(JI)V")))); + assertThat(new MethodDescriptor("([[Z)V"), is(not(new MethodDescriptor("([[LFoo;)V")))); + assertThat(new MethodDescriptor("(LFoo;LBar;)V"), is(not(new MethodDescriptor("(LFoo;LCow;)V")))); + assertThat(new MethodDescriptor("([LFoo;LBar;)V"), is(not(new MethodDescriptor("(LFoo;LCow;)V")))); + + // return desc + assertThat(new MethodDescriptor("()I"), is(new MethodDescriptor("()I"))); + assertThat(new MethodDescriptor("()Z"), is(new MethodDescriptor("()Z"))); + assertThat(new MethodDescriptor("()[D"), is(new MethodDescriptor("()[D"))); + assertThat(new MethodDescriptor("()[[[Z"), is(new MethodDescriptor("()[[[Z"))); + assertThat(new MethodDescriptor("()LFoo;"), is(new MethodDescriptor("()LFoo;"))); + assertThat(new MethodDescriptor("()[LFoo;"), is(new MethodDescriptor("()[LFoo;"))); + + assertThat(new MethodDescriptor("()I"), is(not(new MethodDescriptor("()Z")))); + assertThat(new MethodDescriptor("()Z"), is(not(new MethodDescriptor("()I")))); + assertThat(new MethodDescriptor("()[D"), is(not(new MethodDescriptor("()[J")))); + assertThat(new MethodDescriptor("()[[[Z"), is(not(new MethodDescriptor("()[[Z")))); + assertThat(new MethodDescriptor("()LFoo;"), is(not(new MethodDescriptor("()LBar;")))); + assertThat(new MethodDescriptor("()[LFoo;"), is(not(new MethodDescriptor("()[LBar;")))); + } + + @Test + public void testToString() { + assertThat(new MethodDescriptor("()V").toString(), is("()V")); + assertThat(new MethodDescriptor("(I)V").toString(), is("(I)V")); + assertThat(new MethodDescriptor("(ZIZ)V").toString(), is("(ZIZ)V")); + assertThat(new MethodDescriptor("(LFoo;)V").toString(), is("(LFoo;)V")); + assertThat(new MethodDescriptor("(LFoo;LBar;)V").toString(), is("(LFoo;LBar;)V")); + assertThat(new MethodDescriptor("([I)V").toString(), is("([I)V")); + assertThat(new MethodDescriptor("([[D[[[J)V").toString(), is("([[D[[[J)V")); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/TestTokensConstructors.java b/enigma/src/test/java/cuchaz/enigma/TestTokensConstructors.java new file mode 100644 index 0000000..0398de4 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TestTokensConstructors.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import org.junit.Test; + +import java.nio.file.Paths; + +import static cuchaz.enigma.TestEntryFactory.newBehaviorReferenceByMethod; +import static cuchaz.enigma.TestEntryFactory.newMethod; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class TestTokensConstructors extends TokenChecker { + + public TestTokensConstructors() + throws Exception { + super(Paths.get("build/test-obf/constructors.jar")); + } + + @Test + public void baseDeclarations() { + assertThat(getDeclarationToken(newMethod("a", "", "()V")), is("a")); + assertThat(getDeclarationToken(newMethod("a", "", "(I)V")), is("a")); + } + + @Test + public void subDeclarations() { + assertThat(getDeclarationToken(newMethod("d", "", "()V")), is("d")); + assertThat(getDeclarationToken(newMethod("d", "", "(I)V")), is("d")); + assertThat(getDeclarationToken(newMethod("d", "", "(II)V")), is("d")); + assertThat(getDeclarationToken(newMethod("d", "", "(III)V")), is("d")); + } + + @Test + public void subsubDeclarations() { + assertThat(getDeclarationToken(newMethod("e", "", "(I)V")), is("e")); + } + + @Test + public void defaultDeclarations() { + assertThat(getDeclarationToken(newMethod("c", "", "()V")), nullValue()); + } + + @Test + public void baseDefaultReferences() { + MethodEntry source = newMethod("a", "", "()V"); + assertThat( + getReferenceTokens(newBehaviorReferenceByMethod(source, "b", "a", "()V")), + containsInAnyOrder("a") + ); + assertThat( + getReferenceTokens(newBehaviorReferenceByMethod(source, "d", "", "()V")), + is(empty()) // implicit call, not decompiled to token + ); + assertThat( + getReferenceTokens(newBehaviorReferenceByMethod(source, "d", "", "(III)V")), + is(empty()) // implicit call, not decompiled to token + ); + } + + @Test + public void baseIntReferences() { + MethodEntry source = newMethod("a", "", "(I)V"); + assertThat( + getReferenceTokens(newBehaviorReferenceByMethod(source, "b", "b", "()V")), + containsInAnyOrder("a") + ); + } + + @Test + public void subDefaultReferences() { + MethodEntry source = newMethod("d", "", "()V"); + assertThat( + getReferenceTokens(newBehaviorReferenceByMethod(source, "b", "c", "()V")), + containsInAnyOrder("d") + ); + assertThat( + getReferenceTokens(newBehaviorReferenceByMethod(source, "d", "", "(I)V")), + containsInAnyOrder("this") + ); + } + + @Test + public void subIntReferences() { + MethodEntry source = newMethod("d", "", "(I)V"); + assertThat(getReferenceTokens( + newBehaviorReferenceByMethod(source, "b", "d", "()V")), + containsInAnyOrder("d") + ); + assertThat(getReferenceTokens( + newBehaviorReferenceByMethod(source, "d", "", "(II)V")), + containsInAnyOrder("this") + ); + assertThat(getReferenceTokens( + newBehaviorReferenceByMethod(source, "e", "", "(I)V")), + containsInAnyOrder("super") + ); + } + + @Test + public void subIntIntReferences() { + MethodEntry source = newMethod("d", "", "(II)V"); + assertThat( + getReferenceTokens(newBehaviorReferenceByMethod(source, "b", "e", "()V")), + containsInAnyOrder("d") + ); + } + + @Test + public void subsubIntReferences() { + MethodEntry source = newMethod("e", "", "(I)V"); + assertThat( + getReferenceTokens(newBehaviorReferenceByMethod(source, "b", "f", "()V")), + containsInAnyOrder("e") + ); + } + + @Test + public void defaultConstructableReferences() { + MethodEntry source = newMethod("c", "", "()V"); + assertThat( + getReferenceTokens(newBehaviorReferenceByMethod(source, "b", "g", "()V")), + containsInAnyOrder("c") + ); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/TestTranslator.java b/enigma/src/test/java/cuchaz/enigma/TestTranslator.java new file mode 100644 index 0000000..a420afe --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TestTranslator.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import cuchaz.enigma.translation.representation.entry.Entry; +import org.junit.BeforeClass; +import org.junit.Test; + +import static cuchaz.enigma.TestEntryFactory.*; + +public class TestTranslator { + + @BeforeClass + public static void beforeClass() + throws Exception { + //TODO FIx + //deobfuscator = new Enigma(new JarFile("build/test-obf/translation.jar")); + //try (InputStream in = TestTranslator.class.getResourceAsStream("/cuchaz/enigma/resources/translation.mappings")) { + // mappings = new MappingsJsonReader().read(new InputStreamReader(in)); + // deobfuscator.setMappings(mappings); + // deobfTranslator = deobfuscator.getTranslator(TranslationDirection.Deobfuscating); + // obfTranslator = deobfuscator.getTranslator(TranslationDirection.Obfuscating); + //} + } + + @Test + public void basicClasses() { + assertMapping(newClass("a"), newClass("deobf/A_Basic")); + assertMapping(newClass("b"), newClass("deobf/B_BaseClass")); + assertMapping(newClass("c"), newClass("deobf/C_SubClass")); + } + + @Test + public void basicFields() { + assertMapping(newField("a", "a", "I"), newField("deobf/A_Basic", "f1", "I")); + assertMapping(newField("a", "a", "F"), newField("deobf/A_Basic", "f2", "F")); + assertMapping(newField("a", "a", "Ljava/lang/String;"), newField("deobf/A_Basic", "f3", "Ljava/lang/String;")); + } + + @Test + public void basicMethods() { + assertMapping(newMethod("a", "a", "()V"), newMethod("deobf/A_Basic", "m1", "()V")); + assertMapping(newMethod("a", "a", "()I"), newMethod("deobf/A_Basic", "m2", "()I")); + assertMapping(newMethod("a", "a", "(I)V"), newMethod("deobf/A_Basic", "m3", "(I)V")); + assertMapping(newMethod("a", "a", "(I)I"), newMethod("deobf/A_Basic", "m4", "(I)I")); + } + + // TODO: basic constructors + + @Test + public void inheritanceFields() { + assertMapping(newField("b", "a", "I"), newField("deobf/B_BaseClass", "f1", "I")); + assertMapping(newField("b", "a", "C"), newField("deobf/B_BaseClass", "f2", "C")); + assertMapping(newField("c", "b", "I"), newField("deobf/C_SubClass", "f3", "I")); + assertMapping(newField("c", "c", "I"), newField("deobf/C_SubClass", "f4", "I")); + } + + @Test + public void inheritanceFieldsShadowing() { + assertMapping(newField("c", "b", "C"), newField("deobf/C_SubClass", "f2", "C")); + } + + @Test + public void inheritanceFieldsBySubClass() { + assertMapping(newField("c", "a", "I"), newField("deobf/C_SubClass", "f1", "I")); + // NOTE: can't reference b.C by subclass since it's shadowed + } + + @Test + public void inheritanceMethods() { + assertMapping(newMethod("b", "a", "()I"), newMethod("deobf/B_BaseClass", "m1", "()I")); + assertMapping(newMethod("b", "b", "()I"), newMethod("deobf/B_BaseClass", "m2", "()I")); + assertMapping(newMethod("c", "c", "()I"), newMethod("deobf/C_SubClass", "m3", "()I")); + } + + @Test + public void inheritanceMethodsOverrides() { + assertMapping(newMethod("c", "a", "()I"), newMethod("deobf/C_SubClass", "m1", "()I")); + } + + @Test + public void inheritanceMethodsBySubClass() { + assertMapping(newMethod("c", "b", "()I"), newMethod("deobf/C_SubClass", "m2", "()I")); + } + + @Test + public void innerClasses() { + + // classes + assertMapping(newClass("g"), newClass("deobf/G_OuterClass")); + assertMapping(newClass("g$a"), newClass("deobf/G_OuterClass$A_InnerClass")); + assertMapping(newClass("g$a$a"), newClass("deobf/G_OuterClass$A_InnerClass$A_InnerInnerClass")); + assertMapping(newClass("g$b"), newClass("deobf/G_OuterClass$b")); + assertMapping(newClass("g$b$a"), newClass("deobf/G_OuterClass$b$A_NamedInnerClass")); + + // fields + assertMapping(newField("g$a", "a", "I"), newField("deobf/G_OuterClass$A_InnerClass", "f1", "I")); + assertMapping(newField("g$a", "a", "Ljava/lang/String;"), newField("deobf/G_OuterClass$A_InnerClass", "f2", "Ljava/lang/String;")); + assertMapping(newField("g$a$a", "a", "I"), newField("deobf/G_OuterClass$A_InnerClass$A_InnerInnerClass", "f3", "I")); + assertMapping(newField("g$b$a", "a", "I"), newField("deobf/G_OuterClass$b$A_NamedInnerClass", "f4", "I")); + + // methods + assertMapping(newMethod("g$a", "a", "()V"), newMethod("deobf/G_OuterClass$A_InnerClass", "m1", "()V")); + assertMapping(newMethod("g$a$a", "a", "()V"), newMethod("deobf/G_OuterClass$A_InnerClass$A_InnerInnerClass", "m2", "()V")); + } + + @Test + public void namelessClass() { + assertMapping(newClass("h"), newClass("h")); + } + + @Test + public void testGenerics() { + + // classes + assertMapping(newClass("i"), newClass("deobf/I_Generics")); + assertMapping(newClass("i$a"), newClass("deobf/I_Generics$A_Type")); + assertMapping(newClass("i$b"), newClass("deobf/I_Generics$B_Generic")); + + // fields + assertMapping(newField("i", "a", "Ljava/util/List;"), newField("deobf/I_Generics", "f1", "Ljava/util/List;")); + assertMapping(newField("i", "b", "Ljava/util/List;"), newField("deobf/I_Generics", "f2", "Ljava/util/List;")); + assertMapping(newField("i", "a", "Ljava/util/Map;"), newField("deobf/I_Generics", "f3", "Ljava/util/Map;")); + assertMapping(newField("i$b", "a", "Ljava/lang/Object;"), newField("deobf/I_Generics$B_Generic", "f4", "Ljava/lang/Object;")); + assertMapping(newField("i", "a", "Li$b;"), newField("deobf/I_Generics", "f5", "Ldeobf/I_Generics$B_Generic;")); + assertMapping(newField("i", "b", "Li$b;"), newField("deobf/I_Generics", "f6", "Ldeobf/I_Generics$B_Generic;")); + + // methods + assertMapping(newMethod("i$b", "a", "()Ljava/lang/Object;"), newMethod("deobf/I_Generics$B_Generic", "m1", "()Ljava/lang/Object;")); + } + + private void assertMapping(Entry obf, Entry deobf) { + //assertThat(deobfTranslator.translateEntry(obf), is(deobf)); + //assertThat(obfTranslator.translateEntry(deobf), is(obf)); + + //String deobfName = deobfTranslator.translate(obf); + //if (deobfName != null) { + // assertThat(deobfName, is(deobf.getName())); + //} + + //String obfName = obfTranslator.translate(deobf); + //if (obfName != null) { + // assertThat(obfName, is(obf.getName())); + //} + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/TestTypeDescriptor.java b/enigma/src/test/java/cuchaz/enigma/TestTypeDescriptor.java new file mode 100644 index 0000000..b9ebe55 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TestTypeDescriptor.java @@ -0,0 +1,243 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import cuchaz.enigma.translation.representation.TypeDescriptor; +import org.junit.Test; + +import static cuchaz.enigma.TestEntryFactory.newClass; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +public class TestTypeDescriptor { + + @Test + public void isVoid() { + assertThat(new TypeDescriptor("V").isVoid(), is(true)); + assertThat(new TypeDescriptor("Z").isVoid(), is(false)); + assertThat(new TypeDescriptor("B").isVoid(), is(false)); + assertThat(new TypeDescriptor("C").isVoid(), is(false)); + assertThat(new TypeDescriptor("I").isVoid(), is(false)); + assertThat(new TypeDescriptor("J").isVoid(), is(false)); + assertThat(new TypeDescriptor("F").isVoid(), is(false)); + assertThat(new TypeDescriptor("D").isVoid(), is(false)); + assertThat(new TypeDescriptor("LFoo;").isVoid(), is(false)); + assertThat(new TypeDescriptor("[I").isVoid(), is(false)); + } + + @Test + public void isPrimitive() { + assertThat(new TypeDescriptor("V").isPrimitive(), is(false)); + assertThat(new TypeDescriptor("Z").isPrimitive(), is(true)); + assertThat(new TypeDescriptor("B").isPrimitive(), is(true)); + assertThat(new TypeDescriptor("C").isPrimitive(), is(true)); + assertThat(new TypeDescriptor("I").isPrimitive(), is(true)); + assertThat(new TypeDescriptor("J").isPrimitive(), is(true)); + assertThat(new TypeDescriptor("F").isPrimitive(), is(true)); + assertThat(new TypeDescriptor("D").isPrimitive(), is(true)); + assertThat(new TypeDescriptor("LFoo;").isPrimitive(), is(false)); + assertThat(new TypeDescriptor("[I").isPrimitive(), is(false)); + } + + @Test + public void getPrimitive() { + assertThat(new TypeDescriptor("Z").getPrimitive(), is(TypeDescriptor.Primitive.BOOLEAN)); + assertThat(new TypeDescriptor("B").getPrimitive(), is(TypeDescriptor.Primitive.BYTE)); + assertThat(new TypeDescriptor("C").getPrimitive(), is(TypeDescriptor.Primitive.CHARACTER)); + assertThat(new TypeDescriptor("I").getPrimitive(), is(TypeDescriptor.Primitive.INTEGER)); + assertThat(new TypeDescriptor("J").getPrimitive(), is(TypeDescriptor.Primitive.LONG)); + assertThat(new TypeDescriptor("F").getPrimitive(), is(TypeDescriptor.Primitive.FLOAT)); + assertThat(new TypeDescriptor("D").getPrimitive(), is(TypeDescriptor.Primitive.DOUBLE)); + } + + @Test + public void isClass() { + assertThat(new TypeDescriptor("V").isType(), is(false)); + assertThat(new TypeDescriptor("Z").isType(), is(false)); + assertThat(new TypeDescriptor("B").isType(), is(false)); + assertThat(new TypeDescriptor("C").isType(), is(false)); + assertThat(new TypeDescriptor("I").isType(), is(false)); + assertThat(new TypeDescriptor("J").isType(), is(false)); + assertThat(new TypeDescriptor("F").isType(), is(false)); + assertThat(new TypeDescriptor("D").isType(), is(false)); + assertThat(new TypeDescriptor("LFoo;").isType(), is(true)); + assertThat(new TypeDescriptor("[I").isType(), is(false)); + } + + @Test + public void getClassEntry() { + assertThat(new TypeDescriptor("LFoo;").getTypeEntry(), is(newClass("Foo"))); + assertThat(new TypeDescriptor("Ljava/lang/String;").getTypeEntry(), is(newClass("java/lang/String"))); + } + + @Test + public void getArrayClassEntry() { + assertThat(new TypeDescriptor("[LFoo;").getTypeEntry(), is(newClass("Foo"))); + assertThat(new TypeDescriptor("[[[Ljava/lang/String;").getTypeEntry(), is(newClass("java/lang/String"))); + } + + @Test + public void isArray() { + assertThat(new TypeDescriptor("V").isArray(), is(false)); + assertThat(new TypeDescriptor("Z").isArray(), is(false)); + assertThat(new TypeDescriptor("B").isArray(), is(false)); + assertThat(new TypeDescriptor("C").isArray(), is(false)); + assertThat(new TypeDescriptor("I").isArray(), is(false)); + assertThat(new TypeDescriptor("J").isArray(), is(false)); + assertThat(new TypeDescriptor("F").isArray(), is(false)); + assertThat(new TypeDescriptor("D").isArray(), is(false)); + assertThat(new TypeDescriptor("LFoo;").isArray(), is(false)); + assertThat(new TypeDescriptor("[I").isArray(), is(true)); + } + + @Test + public void getArrayDimension() { + assertThat(new TypeDescriptor("[I").getArrayDimension(), is(1)); + assertThat(new TypeDescriptor("[[I").getArrayDimension(), is(2)); + assertThat(new TypeDescriptor("[[[I").getArrayDimension(), is(3)); + } + + @Test + public void getArrayType() { + assertThat(new TypeDescriptor("[I").getArrayType(), is(new TypeDescriptor("I"))); + assertThat(new TypeDescriptor("[[I").getArrayType(), is(new TypeDescriptor("I"))); + assertThat(new TypeDescriptor("[[[I").getArrayType(), is(new TypeDescriptor("I"))); + assertThat(new TypeDescriptor("[Ljava/lang/String;").getArrayType(), is(new TypeDescriptor("Ljava/lang/String;"))); + } + + @Test + public void hasClass() { + assertThat(new TypeDescriptor("LFoo;").containsType(), is(true)); + assertThat(new TypeDescriptor("Ljava/lang/String;").containsType(), is(true)); + assertThat(new TypeDescriptor("[LBar;").containsType(), is(true)); + assertThat(new TypeDescriptor("[[[LCat;").containsType(), is(true)); + + assertThat(new TypeDescriptor("V").containsType(), is(false)); + assertThat(new TypeDescriptor("[I").containsType(), is(false)); + assertThat(new TypeDescriptor("[[[I").containsType(), is(false)); + assertThat(new TypeDescriptor("Z").containsType(), is(false)); + } + + @Test + public void parseVoid() { + final String answer = "V"; + assertThat(TypeDescriptor.parseFirst("V"), is(answer)); + assertThat(TypeDescriptor.parseFirst("VVV"), is(answer)); + assertThat(TypeDescriptor.parseFirst("VIJ"), is(answer)); + assertThat(TypeDescriptor.parseFirst("V[I"), is(answer)); + assertThat(TypeDescriptor.parseFirst("VLFoo;"), is(answer)); + assertThat(TypeDescriptor.parseFirst("V[LFoo;"), is(answer)); + } + + @Test + public void parsePrimitive() { + final String answer = "I"; + assertThat(TypeDescriptor.parseFirst("I"), is(answer)); + assertThat(TypeDescriptor.parseFirst("III"), is(answer)); + assertThat(TypeDescriptor.parseFirst("IJZ"), is(answer)); + assertThat(TypeDescriptor.parseFirst("I[I"), is(answer)); + assertThat(TypeDescriptor.parseFirst("ILFoo;"), is(answer)); + assertThat(TypeDescriptor.parseFirst("I[LFoo;"), is(answer)); + } + + @Test + public void parseClass() { + { + final String answer = "LFoo;"; + assertThat(TypeDescriptor.parseFirst("LFoo;"), is(answer)); + assertThat(TypeDescriptor.parseFirst("LFoo;I"), is(answer)); + assertThat(TypeDescriptor.parseFirst("LFoo;JZ"), is(answer)); + assertThat(TypeDescriptor.parseFirst("LFoo;[I"), is(answer)); + assertThat(TypeDescriptor.parseFirst("LFoo;LFoo;"), is(answer)); + assertThat(TypeDescriptor.parseFirst("LFoo;[LFoo;"), is(answer)); + } + { + final String answer = "Ljava/lang/String;"; + assertThat(TypeDescriptor.parseFirst("Ljava/lang/String;"), is(answer)); + assertThat(TypeDescriptor.parseFirst("Ljava/lang/String;I"), is(answer)); + assertThat(TypeDescriptor.parseFirst("Ljava/lang/String;JZ"), is(answer)); + assertThat(TypeDescriptor.parseFirst("Ljava/lang/String;[I"), is(answer)); + assertThat(TypeDescriptor.parseFirst("Ljava/lang/String;LFoo;"), is(answer)); + assertThat(TypeDescriptor.parseFirst("Ljava/lang/String;[LFoo;"), is(answer)); + } + } + + @Test + public void parseArray() { + { + final String answer = "[I"; + assertThat(TypeDescriptor.parseFirst("[I"), is(answer)); + assertThat(TypeDescriptor.parseFirst("[III"), is(answer)); + assertThat(TypeDescriptor.parseFirst("[IJZ"), is(answer)); + assertThat(TypeDescriptor.parseFirst("[I[I"), is(answer)); + assertThat(TypeDescriptor.parseFirst("[ILFoo;"), is(answer)); + } + { + final String answer = "[[I"; + assertThat(TypeDescriptor.parseFirst("[[I"), is(answer)); + assertThat(TypeDescriptor.parseFirst("[[III"), is(answer)); + assertThat(TypeDescriptor.parseFirst("[[IJZ"), is(answer)); + assertThat(TypeDescriptor.parseFirst("[[I[I"), is(answer)); + assertThat(TypeDescriptor.parseFirst("[[ILFoo;"), is(answer)); + } + { + final String answer = "[LFoo;"; + assertThat(TypeDescriptor.parseFirst("[LFoo;"), is(answer)); + assertThat(TypeDescriptor.parseFirst("[LFoo;II"), is(answer)); + assertThat(TypeDescriptor.parseFirst("[LFoo;JZ"), is(answer)); + assertThat(TypeDescriptor.parseFirst("[LFoo;[I"), is(answer)); + assertThat(TypeDescriptor.parseFirst("[LFoo;LFoo;"), is(answer)); + } + } + + @Test + public void equals() { + assertThat(new TypeDescriptor("V"), is(new TypeDescriptor("V"))); + assertThat(new TypeDescriptor("Z"), is(new TypeDescriptor("Z"))); + assertThat(new TypeDescriptor("B"), is(new TypeDescriptor("B"))); + assertThat(new TypeDescriptor("C"), is(new TypeDescriptor("C"))); + assertThat(new TypeDescriptor("I"), is(new TypeDescriptor("I"))); + assertThat(new TypeDescriptor("J"), is(new TypeDescriptor("J"))); + assertThat(new TypeDescriptor("F"), is(new TypeDescriptor("F"))); + assertThat(new TypeDescriptor("D"), is(new TypeDescriptor("D"))); + assertThat(new TypeDescriptor("LFoo;"), is(new TypeDescriptor("LFoo;"))); + assertThat(new TypeDescriptor("[I"), is(new TypeDescriptor("[I"))); + assertThat(new TypeDescriptor("[[[I"), is(new TypeDescriptor("[[[I"))); + assertThat(new TypeDescriptor("[LFoo;"), is(new TypeDescriptor("[LFoo;"))); + + assertThat(new TypeDescriptor("V"), is(not(new TypeDescriptor("I")))); + assertThat(new TypeDescriptor("I"), is(not(new TypeDescriptor("J")))); + assertThat(new TypeDescriptor("I"), is(not(new TypeDescriptor("LBar;")))); + assertThat(new TypeDescriptor("I"), is(not(new TypeDescriptor("[I")))); + assertThat(new TypeDescriptor("LFoo;"), is(not(new TypeDescriptor("LBar;")))); + assertThat(new TypeDescriptor("[I"), is(not(new TypeDescriptor("[Z")))); + assertThat(new TypeDescriptor("[[[I"), is(not(new TypeDescriptor("[I")))); + assertThat(new TypeDescriptor("[LFoo;"), is(not(new TypeDescriptor("[LBar;")))); + } + + @Test + public void testToString() { + assertThat(new TypeDescriptor("V").toString(), is("V")); + assertThat(new TypeDescriptor("Z").toString(), is("Z")); + assertThat(new TypeDescriptor("B").toString(), is("B")); + assertThat(new TypeDescriptor("C").toString(), is("C")); + assertThat(new TypeDescriptor("I").toString(), is("I")); + assertThat(new TypeDescriptor("J").toString(), is("J")); + assertThat(new TypeDescriptor("F").toString(), is("F")); + assertThat(new TypeDescriptor("D").toString(), is("D")); + assertThat(new TypeDescriptor("LFoo;").toString(), is("LFoo;")); + assertThat(new TypeDescriptor("[I").toString(), is("[I")); + assertThat(new TypeDescriptor("[[[I").toString(), is("[[[I")); + assertThat(new TypeDescriptor("[LFoo;").toString(), is("[LFoo;")); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/TokenChecker.java b/enigma/src/test/java/cuchaz/enigma/TokenChecker.java new file mode 100644 index 0000000..96fc6da --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/TokenChecker.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma; + +import com.google.common.collect.Lists; +import cuchaz.enigma.analysis.ClassCache; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.source.*; +import cuchaz.enigma.source.Token; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; + +public class TokenChecker { + private final Decompiler decompiler; + + protected TokenChecker(Path path) throws IOException { + ClassCache classCache = ClassCache.of(path); + decompiler = Decompilers.PROCYON.create(classCache, new SourceSettings(false, false)); + } + + protected String getDeclarationToken(Entry entry) { + // decompile the class + Source source = decompiler.getSource(entry.getContainingClass().getFullName()); + // DEBUG + // tree.acceptVisitor( new TreeDumpVisitor( new File( "tree." + entry.getClassName().replace( '/', '.' ) + ".txt" ) ), null ); + String string = source.asString(); + SourceIndex index = source.index(); + + // get the token value + Token token = index.getDeclarationToken(entry); + if (token == null) { + return null; + } + return string.substring(token.start, token.end); + } + + @SuppressWarnings("unchecked") + protected Collection getReferenceTokens(EntryReference, ? extends Entry> reference) { + // decompile the class + Source source = decompiler.getSource(reference.context.getContainingClass().getFullName()); + String string = source.asString(); + SourceIndex index = source.index(); + + // get the token values + List values = Lists.newArrayList(); + for (Token token : index.getReferenceTokens((EntryReference, Entry>) reference)) { + values.add(string.substring(token.start, token.end)); + } + return values; + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/Keep.java b/enigma/src/test/java/cuchaz/enigma/inputs/Keep.java new file mode 100644 index 0000000..4dbe8e2 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/Keep.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs; + +public class Keep { + public static void main(String[] args) { + System.out.println("Keep me!"); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/constructors/BaseClass.java b/enigma/src/test/java/cuchaz/enigma/inputs/constructors/BaseClass.java new file mode 100644 index 0000000..f07e1f8 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/constructors/BaseClass.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.constructors; + +// a +public class BaseClass { + + // ()V + public BaseClass() { + System.out.println("Default constructor"); + } + + // (I)V + public BaseClass(int i) { + System.out.println("Int constructor " + i); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/constructors/Caller.java b/enigma/src/test/java/cuchaz/enigma/inputs/constructors/Caller.java new file mode 100644 index 0000000..71439fd --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/constructors/Caller.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.constructors; + +// b +public class Caller { + + // a()V + public void callBaseDefault() { + // a.()V + System.out.println(new BaseClass()); + } + + // b()V + public void callBaseInt() { + // a.(I)V + System.out.println(new BaseClass(5)); + } + + // c()V + public void callSubDefault() { + // d.()V + System.out.println(new SubClass()); + } + + // d()V + public void callSubInt() { + // d.(I)V + System.out.println(new SubClass(6)); + } + + // e()V + public void callSubIntInt() { + // d.(II)V + System.out.println(new SubClass(4, 2)); + } + + // f()V + public void callSubSubInt() { + // e.(I)V + System.out.println(new SubSubClass(3)); + } + + // g()V + public void callDefaultConstructable() { + // c.()V + System.out.println(new DefaultConstructable()); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/constructors/DefaultConstructable.java b/enigma/src/test/java/cuchaz/enigma/inputs/constructors/DefaultConstructable.java new file mode 100644 index 0000000..c3d4170 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/constructors/DefaultConstructable.java @@ -0,0 +1,16 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.constructors; + +public class DefaultConstructable { + // only default constructor +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/constructors/SubClass.java b/enigma/src/test/java/cuchaz/enigma/inputs/constructors/SubClass.java new file mode 100644 index 0000000..bc56b3b --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/constructors/SubClass.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.constructors; + +// d extends a +public class SubClass extends BaseClass { + + // ()V + public SubClass() { + // a.()V + } + + // (I)V + public SubClass(int num) { + // ()V + this(); + System.out.println("SubClass " + num); + } + + // (II)V + public SubClass(int a, int b) { + // (I)V + this(a + b); + } + + // (III)V + public SubClass(int a, int b, int c) { + // a.()V + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/constructors/SubSubClass.java b/enigma/src/test/java/cuchaz/enigma/inputs/constructors/SubSubClass.java new file mode 100644 index 0000000..87b69d3 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/constructors/SubSubClass.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.constructors; + +// e extends d +public class SubSubClass extends SubClass { + + // (I)V + public SubSubClass(int i) { + // c.(I)V + super(i); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/BaseClass.java b/enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/BaseClass.java new file mode 100644 index 0000000..b9c4929 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/BaseClass.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.inheritanceTree; + +// a +public abstract class BaseClass { + + // a + private String name; + + // (Ljava/lang/String;)V + protected BaseClass(String name) { + this.name = name; + } + + // a()Ljava/lang/String; + public String getName() { + return name; + } + + // a()V + public abstract void doBaseThings(); +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/SubclassA.java b/enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/SubclassA.java new file mode 100644 index 0000000..50e963c --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/SubclassA.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.inheritanceTree; + +// b extends a +public abstract class SubclassA extends BaseClass { + + // (Ljava/lang/String;)V + protected SubclassA(String name) { + // call to a.(Ljava/lang/String)V + super(name); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/SubclassB.java b/enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/SubclassB.java new file mode 100644 index 0000000..d0dd664 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/SubclassB.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.inheritanceTree; + +// c extends a +public class SubclassB extends BaseClass { + + // a + private int numThings; + + // ()V + protected SubclassB() { + // a.(Ljava/lang/String;)V + super("B"); + + // access to a + numThings = 4; + } + + @Override + // a()V + public void doBaseThings() { + // call to a.a()Ljava/lang/String; + System.out.println("Base things by B! " + getName()); + } + + // b()V + public void doBThings() { + // access to a + System.out.println("" + numThings + " B things!"); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/SubsubclassAA.java b/enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/SubsubclassAA.java new file mode 100644 index 0000000..c584570 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/inheritanceTree/SubsubclassAA.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.inheritanceTree; + +// d extends b +public class SubsubclassAA extends SubclassA { + + protected SubsubclassAA() { + // call to b.(Ljava/lang/String;)V + super("AA"); + } + + @Override + // a()Ljava/lang/String; + public String getName() { + // call to b.a()Ljava/lang/String; + return "subsub" + super.getName(); + } + + @Override + // a()V + public void doBaseThings() { + // call to d.a()Ljava/lang/String; + System.out.println("Base things by " + getName()); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/A_Anonymous.java b/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/A_Anonymous.java new file mode 100644 index 0000000..f652d87 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/A_Anonymous.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.innerClasses; + +public class A_Anonymous { + + public void foo() { + Runnable runnable = new Runnable() { + @Override + public void run() { + // don't care + } + }; + runnable.run(); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/B_AnonymousWithScopeArgs.java b/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/B_AnonymousWithScopeArgs.java new file mode 100644 index 0000000..d1b7601 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/B_AnonymousWithScopeArgs.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.innerClasses; + +public class B_AnonymousWithScopeArgs { + + public static void foo(final D_Simple arg) { + System.out.println(new Object() { + @Override + public String toString() { + return arg.toString(); + } + }); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/C_ConstructorArgs.java b/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/C_ConstructorArgs.java new file mode 100644 index 0000000..94061fa --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/C_ConstructorArgs.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.innerClasses; + +@SuppressWarnings("unused") +public class C_ConstructorArgs { + + Inner i; + + public void foo() { + i = new Inner(5); + } + + class Inner { + + private int a; + + public Inner(int a) { + this.a = a; + } + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/D_Simple.java b/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/D_Simple.java new file mode 100644 index 0000000..71b3a6d --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/D_Simple.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.innerClasses; + +public class D_Simple { + + class Inner { + // nothing to do + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/E_AnonymousWithOuterAccess.java b/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/E_AnonymousWithOuterAccess.java new file mode 100644 index 0000000..976ec42 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/E_AnonymousWithOuterAccess.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.innerClasses; + +public class E_AnonymousWithOuterAccess { + + // reproduction of error case documented at: + // https://bitbucket.org/cuchaz/enigma/issue/61/stackoverflowerror-when-deobfuscating + + public Object makeInner() { + outerMethod(); + return new Object() { + @Override + public String toString() { + return outerMethod(); + } + }; + } + + private String outerMethod() { + return "foo"; + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/F_ClassTree.java b/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/F_ClassTree.java new file mode 100644 index 0000000..b1de3c9 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/innerClasses/F_ClassTree.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.innerClasses; + +public class F_ClassTree { + + public class Level1 { + + public int f1; + + public class Level2 { + + public int f2; + + public class Level3 { + + public int f3; + } + } + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/loneClass/LoneClass.java b/enigma/src/test/java/cuchaz/enigma/inputs/loneClass/LoneClass.java new file mode 100644 index 0000000..ddc4e31 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/loneClass/LoneClass.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.loneClass; + +public class LoneClass { + + private String name; + + public LoneClass(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/packageAccess/Base.java b/enigma/src/test/java/cuchaz/enigma/inputs/packageAccess/Base.java new file mode 100644 index 0000000..6f5fe30 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/packageAccess/Base.java @@ -0,0 +1,7 @@ +package cuchaz.enigma.inputs.packageAccess; + +public class Base { + protected int make() { + return 42; + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/packageAccess/SamePackageChild.java b/enigma/src/test/java/cuchaz/enigma/inputs/packageAccess/SamePackageChild.java new file mode 100644 index 0000000..cf0f657 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/packageAccess/SamePackageChild.java @@ -0,0 +1,12 @@ +package cuchaz.enigma.inputs.packageAccess; + +public class SamePackageChild extends Base { + + class Inner { + final int value; + + Inner() { + value = SamePackageChild.this.make(); // no synthetic method + } + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/packageAccess/sub/OtherPackageChild.java b/enigma/src/test/java/cuchaz/enigma/inputs/packageAccess/sub/OtherPackageChild.java new file mode 100644 index 0000000..19fb19c --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/packageAccess/sub/OtherPackageChild.java @@ -0,0 +1,14 @@ +package cuchaz.enigma.inputs.packageAccess.sub; + +import cuchaz.enigma.inputs.packageAccess.Base; + +public class OtherPackageChild extends Base { + + class Inner { + final int value; + + Inner() { + value = OtherPackageChild.this.make(); // synthetic method call + } + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/translation/A_Basic.java b/enigma/src/test/java/cuchaz/enigma/inputs/translation/A_Basic.java new file mode 100644 index 0000000..26f3718 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/translation/A_Basic.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.translation; + +public class A_Basic { + + public int one; + public float two; + public String three; + + public void m1() { + } + + public int m2() { + return 42; + } + + public void m3(int a1) { + } + + public int m4(int a1) { + return 5; // chosen by fair die roll, guaranteed to be random + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/translation/B_BaseClass.java b/enigma/src/test/java/cuchaz/enigma/inputs/translation/B_BaseClass.java new file mode 100644 index 0000000..fd7f6e7 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/translation/B_BaseClass.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.translation; + +public class B_BaseClass { + + public int f1; + public char f2; + + public int m1() { + return 5; + } + + public int m2() { + return 42; + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/translation/C_SubClass.java b/enigma/src/test/java/cuchaz/enigma/inputs/translation/C_SubClass.java new file mode 100644 index 0000000..9d74e44 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/translation/C_SubClass.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.translation; + +public class C_SubClass extends B_BaseClass { + + public char f2; // shadows B_BaseClass.f2 + public int f3; + public int f4; + + @Override + public int m1() { + return 32; + } + + public int m3() { + return 7; + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/translation/D_AnonymousTesting.java b/enigma/src/test/java/cuchaz/enigma/inputs/translation/D_AnonymousTesting.java new file mode 100644 index 0000000..99c83bb --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/translation/D_AnonymousTesting.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.translation; + +import java.util.ArrayList; +import java.util.List; + +public class D_AnonymousTesting { + + public List getObjs() { + List objs = new ArrayList(); + objs.add(new Object() { + @Override + public String toString() { + return "Object!"; + } + }); + return objs; + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/translation/E_Bridges.java b/enigma/src/test/java/cuchaz/enigma/inputs/translation/E_Bridges.java new file mode 100644 index 0000000..0b8cf2a --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/translation/E_Bridges.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.translation; + +import java.util.Iterator; + +public class E_Bridges implements Iterator { + + @Override + public boolean hasNext() { + return false; + } + + @Override + public String next() { + // the compiler will generate a bridge for this method + return "foo"; + } + + @Override + public void remove() { + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/translation/F_ObjectMethods.java b/enigma/src/test/java/cuchaz/enigma/inputs/translation/F_ObjectMethods.java new file mode 100644 index 0000000..8a92792 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/translation/F_ObjectMethods.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.translation; + +@SuppressWarnings("FinalizeCalledExplicitly") +public class F_ObjectMethods { + + public void callEmAll() + throws Throwable { + clone(); + equals(this); + finalize(); + getClass(); + hashCode(); + notify(); + notifyAll(); + toString(); + wait(); + wait(0); + wait(0, 0); + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/translation/G_OuterClass.java b/enigma/src/test/java/cuchaz/enigma/inputs/translation/G_OuterClass.java new file mode 100644 index 0000000..a1e6a85 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/translation/G_OuterClass.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.translation; + +public class G_OuterClass { + + public class A_InnerClass { + + public int f1; + public String f2; + + public void m1() {} + + public class A_InnerInnerClass { + + public int f3; + + public void m2() {} + } + } + + public class B_NamelessClass { + public class A_NamedInnerClass { + public int f4; + } + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/translation/H_NamelessClass.java b/enigma/src/test/java/cuchaz/enigma/inputs/translation/H_NamelessClass.java new file mode 100644 index 0000000..013c55a --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/translation/H_NamelessClass.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.translation; + +public class H_NamelessClass { + + public class A_InnerClass { + + public int f1; + public String f2; + + public void m1() {} + + public class A_InnerInnerClass { + + public int f3; + + public void m2() {} + } + } + + public class B_NamelessClass { + public class A_NamedInnerClass { + public int f4; + + public class A_AnotherInnerClass {} + + public class B_YetAnotherInnerClass {} + } + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/inputs/translation/I_Generics.java b/enigma/src/test/java/cuchaz/enigma/inputs/translation/I_Generics.java new file mode 100644 index 0000000..fd2ebdd --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/inputs/translation/I_Generics.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.inputs.translation; + +import java.util.List; +import java.util.Map; + +public class I_Generics { + + public List f1; + public List f2; + public Map f3; + public B_Generic f5; + public B_Generic f6; + + public class A_Type { + } + + public class B_Generic { + public T f4; + + public T m1() { + return null; + } + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/translation/mapping/TestComments.java b/enigma/src/test/java/cuchaz/enigma/translation/mapping/TestComments.java new file mode 100644 index 0000000..e831943 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/translation/mapping/TestComments.java @@ -0,0 +1,39 @@ +package cuchaz.enigma.translation.mapping; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.mapping.serde.MappingFileNameFormat; +import cuchaz.enigma.translation.mapping.serde.MappingParseException; +import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; +import cuchaz.enigma.translation.mapping.serde.enigma.EnigmaMappingsReader; +import cuchaz.enigma.translation.mapping.serde.tinyv2.TinyV2Writer; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import org.junit.Test; + +public class TestComments { + private static Path DIRECTORY; + + static { + try { + DIRECTORY = Paths.get(TestTinyV2InnerClasses.class.getResource("/comments/").toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + @Test + public void testParseAndWrite() throws IOException, MappingParseException { + ProgressListener progressListener = ProgressListener.none(); + MappingSaveParameters params = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF); + EntryTree mappings = EnigmaMappingsReader.DIRECTORY.read( + DIRECTORY, progressListener, params); + + new TinyV2Writer("intermediary", "named") + .write(mappings, DIRECTORY.resolve("convertedtiny.tiny"), progressListener, params); + } + +} \ No newline at end of file diff --git a/enigma/src/test/java/cuchaz/enigma/translation/mapping/TestTinyV2InnerClasses.java b/enigma/src/test/java/cuchaz/enigma/translation/mapping/TestTinyV2InnerClasses.java new file mode 100644 index 0000000..65941e5 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/translation/mapping/TestTinyV2InnerClasses.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2015 Jeff Martin. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public + * License v3.0 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl.html + * + * Contributors: + * Jeff Martin - initial API and implementation + ******************************************************************************/ + +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.Enigma; +import cuchaz.enigma.EnigmaProject; +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.mapping.serde.enigma.EnigmaMappingsReader; + +import java.nio.file.Path; +import java.nio.file.Paths; + +public final class TestTinyV2InnerClasses { + private Path jar; + private Path mappings; + + public TestTinyV2InnerClasses() throws Exception { + jar = Paths.get("build/test-obf/innerClasses.jar"); + mappings = Paths.get(TestTinyV2InnerClasses.class.getResource("/tinyV2InnerClasses/").toURI()); + } + +// @Test + public void testMappings() throws Exception { + EnigmaProject project = Enigma.create().openJar(jar, ProgressListener.none()); + project.setMappings(EnigmaMappingsReader.DIRECTORY.read(mappings, ProgressListener.none(), project.getEnigma().getProfile().getMappingSaveParameters())); + + } +} diff --git a/enigma/src/test/java/cuchaz/enigma/translation/mapping/TestV2Main.java b/enigma/src/test/java/cuchaz/enigma/translation/mapping/TestV2Main.java new file mode 100644 index 0000000..6e4d7b9 --- /dev/null +++ b/enigma/src/test/java/cuchaz/enigma/translation/mapping/TestV2Main.java @@ -0,0 +1,23 @@ +package cuchaz.enigma.translation.mapping; + +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.translation.mapping.serde.MappingFileNameFormat; +import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; +import cuchaz.enigma.translation.mapping.serde.enigma.EnigmaMappingsReader; +import cuchaz.enigma.translation.mapping.serde.tinyv2.TinyV2Writer; +import cuchaz.enigma.translation.mapping.tree.EntryTree; + +import java.nio.file.Path; +import java.nio.file.Paths; + +public final class TestV2Main { + public static void main(String... args) throws Exception { + Path path = Paths.get(TestV2Main.class.getResource("/tinyV2InnerClasses/").toURI()); + + MappingSaveParameters parameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF); + + EntryTree tree = EnigmaMappingsReader.DIRECTORY.read(path, ProgressListener.none(), parameters); + + new TinyV2Writer("obf", "deobf").write(tree, Paths.get("currentYarn.tiny"), ProgressListener.none(), parameters); + } +} diff --git a/enigma/src/test/resources/comments/test.mapping b/enigma/src/test/resources/comments/test.mapping new file mode 100644 index 0000000..d134558 --- /dev/null +++ b/enigma/src/test/resources/comments/test.mapping @@ -0,0 +1,18 @@ +CLASS net/minecraft/class_1158 net/minecraft/util/math/Quaternion + COMMENT it circel + COMMENT next line + FIELD field_21493 IDENTITY Lnet/minecraft/class_1158; + COMMENT moar comment thing + COMMENT near field + METHOD foo bar (FFFF)V + COMMENT method comment + COMMENT second line + COMMENT third line + ARG 1 b + COMMENT arg comment + CLASS old new + COMMENT inner comment + FIELD field_19263 iterator Lnet/minecraft/class_3980; + METHOD tryAdvance (Ljava/util/function/Consumer;)Z + ARG 1 consumer + COMMENT very inner comment \ No newline at end of file diff --git a/enigma/src/test/resources/proguard-build.conf b/enigma/src/test/resources/proguard-build.conf new file mode 100644 index 0000000..691d8a2 --- /dev/null +++ b/enigma/src/test/resources/proguard-build.conf @@ -0,0 +1,6 @@ +-dontoptimize +-dontobfuscate +-dontwarn +-keep class cuchaz.enigma.Main { static void main(java.lang.String[]); } +-keep class cuchaz.enigma.command.Main { static void main(java.lang.String[]); } +-keep class de.sciss.syntaxpane.** { *; } diff --git a/enigma/src/test/resources/proguard-test.conf b/enigma/src/test/resources/proguard-test.conf new file mode 100644 index 0000000..9411d26 --- /dev/null +++ b/enigma/src/test/resources/proguard-test.conf @@ -0,0 +1,8 @@ +-overloadaggressively +-repackageclasses +-allowaccessmodification +-dontoptimize +-dontshrink +-keepparameternames +-keepattributes +-keep class cuchaz.enigma.inputs.Keep diff --git a/enigma/src/test/resources/tinyV2InnerClasses/c.mapping b/enigma/src/test/resources/tinyV2InnerClasses/c.mapping new file mode 100644 index 0000000..f9b0442 --- /dev/null +++ b/enigma/src/test/resources/tinyV2InnerClasses/c.mapping @@ -0,0 +1,2 @@ +CLASS c + CLASS a Kid diff --git a/enigma/src/test/resources/tinyV2InnerClasses/cuchaz/enigma/Dad.mapping b/enigma/src/test/resources/tinyV2InnerClasses/cuchaz/enigma/Dad.mapping new file mode 100644 index 0000000..8d43ba9 --- /dev/null +++ b/enigma/src/test/resources/tinyV2InnerClasses/cuchaz/enigma/Dad.mapping @@ -0,0 +1,5 @@ +CLASS f cuchaz/enigma/Dad + CLASS a One + CLASS a Two + CLASS a + FIELD a value I diff --git a/enigma/src/test/resources/translation.mappings b/enigma/src/test/resources/translation.mappings new file mode 100644 index 0000000..c08765c --- /dev/null +++ b/enigma/src/test/resources/translation.mappings @@ -0,0 +1,41 @@ +CLASS a deobf/A_Basic + FIELD a f1 I + FIELD a f2 F + FIELD a f3 Ljava/lang/String; + METHOD a m1 ()V + METHOD a m2 ()I + METHOD a m3 (I)V + METHOD a m4 (I)I +CLASS b deobf/B_BaseClass + FIELD a f1 I + FIELD a f2 C + METHOD a m1 ()I + METHOD b m2 ()I +CLASS c deobf/C_SubClass + FIELD b f2 C + FIELD b f3 I + FIELD c f4 I + METHOD a m1 ()I + METHOD c m3 ()I +CLASS g deobf/G_OuterClass + CLASS g$a A_InnerClass + CLASS g$a$a A_InnerInnerClass + FIELD a f3 I + METHOD a m2 ()V + FIELD a f1 I + FIELD a f2 Ljava/lang/String; + METHOD a m1 ()V + CLASS g$b + CLASS g$b$a A_NamedInnerClass + FIELD a f4 I +CLASS h +CLASS i deobf/I_Generics + CLASS i$a A_Type + CLASS i$b B_Generic + FIELD a f4 Ljava/lang/Object; + METHOD a m1 ()Ljava/lang/Object; + FIELD a f1 Ljava/util/List; + FIELD b f2 Ljava/util/List; + FIELD a f3 Ljava/util/Map; + FIELD a f5 Li$b; + FIELD b f6 Li$b; -- cgit v1.2.3